Skip to content

Commit 3bc2575

Browse files
committed
Init
0 parents  commit 3bc2575

File tree

12 files changed

+6053
-0
lines changed

12 files changed

+6053
-0
lines changed

.gitignore

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
2+
# Created by https://www.gitignore.io/api/node
3+
# Edit at https://www.gitignore.io/?templates=node
4+
5+
### Node ###
6+
# Logs
7+
logs
8+
*.log
9+
npm-debug.log*
10+
yarn-debug.log*
11+
yarn-error.log*
12+
13+
# Runtime data
14+
pids
15+
*.pid
16+
*.seed
17+
*.pid.lock
18+
19+
# Directory for instrumented libs generated by jscoverage/JSCover
20+
lib-cov
21+
22+
# Coverage directory used by tools like istanbul
23+
coverage
24+
25+
# nyc test coverage
26+
.nyc_output
27+
28+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29+
.grunt
30+
31+
# Bower dependency directory (https://bower.io/)
32+
bower_components
33+
34+
# node-waf configuration
35+
.lock-wscript
36+
37+
# Compiled binary addons (https://nodejs.org/api/addons.html)
38+
build/Release
39+
40+
# Dependency directories
41+
node_modules/
42+
jspm_packages/
43+
44+
# TypeScript v1 declaration files
45+
typings/
46+
47+
# Optional npm cache directory
48+
.npm
49+
50+
# Optional eslint cache
51+
.eslintcache
52+
53+
# Optional REPL history
54+
.node_repl_history
55+
56+
# Output of 'npm pack'
57+
*.tgz
58+
59+
# Yarn Integrity file
60+
.yarn-integrity
61+
62+
# dotenv environment variables file
63+
.env
64+
65+
# parcel-bundler cache (https://parceljs.org/)
66+
.cache
67+
68+
# next.js build output
69+
.next
70+
71+
# nuxt.js build output
72+
.nuxt
73+
74+
# vuepress build output
75+
.vuepress/dist
76+
77+
# Serverless directories
78+
.serverless
79+
80+
# FuseBox cache
81+
.fusebox/
82+
83+
# End of https://www.gitignore.io/api/node
84+
85+
esm/

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# React hooks for RxJS
2+
3+
## useObservable
4+
5+
```ts
6+
declare function useObservable<T>(sourceFactory: () => Observable<T>): T | null
7+
8+
declare function useObservable<T>(sourceFactory: () => Observable<T>, initialState: T): T
9+
10+
declare function useObservable<T, U>(sourceFactory: (props$: Observable<U>) => Observable<T>, initialState: T, inputs: U): T
11+
```
12+
13+
## useEventCallback
14+
15+
```ts
16+
declare type EventCallbackState<T, U> = [
17+
((e: SyntheticEvent<T>) => void) | typeof noop,
18+
U
19+
]
20+
declare type EventCallback<T, U> = (
21+
eventSource$: Observable<SyntheticEvent<T>>
22+
) => Observable<U>
23+
24+
declare function useEventCallback<T, U = void>(
25+
callback: EventCallback<T, U>
26+
): EventCallbackState<T, U | null>
27+
declare function useEventCallback<T, U = void>(
28+
callback: EventCallback<T, U>,
29+
initialState: U
30+
): EventCallbackState<T, U>
31+
```

index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
7+
<title>React Observable Hooks</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
</body>
12+
</html>

package.json

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"name": "rxjs-hooks",
3+
"version": "0.0.1",
4+
"description": "React hooks for RxJS",
5+
"main": "index.js",
6+
"author": "LongYinan <lynweklm@gmail.com>",
7+
"license": "MIT",
8+
"scripts": {
9+
"build": "rm -rf esm && tsc -p src/tsconfig.json --outDir esm",
10+
"lint": "tslint -c tslint.json -p src/tsconfig.json",
11+
"start": "webpack-dev-server --progress --color"
12+
},
13+
"repository": {
14+
"type": "git",
15+
"url": "git@github.com:LeetCode-OpenSource/rxjs-hooks.git"
16+
},
17+
"devDependencies": {
18+
"@types/lodash": "^4.14.118",
19+
"@types/react-dom": "^16.0.9",
20+
"@types/webpack": "^4.4.19",
21+
"fork-ts-checker-webpack-plugin": "^0.4.15",
22+
"happypack": "^5.0.0",
23+
"html-webpack-plugin": "^3.2.0",
24+
"husky": "^1.1.4",
25+
"lint-staged": "^8.0.4",
26+
"prettier": "^1.15.2",
27+
"react": "16.7.0-alpha.2",
28+
"react-dom": "16.7.0-alpha.2",
29+
"rxjs": "^6.3.3",
30+
"source-map-loader": "^0.2.4",
31+
"standard": "^12.0.1",
32+
"ts-loader": "^5.3.0",
33+
"tslint": "^5.11.0",
34+
"tslint-config-prettier": "^1.16.0",
35+
"tslint-eslint-rules": "^5.4.0",
36+
"tslint-react": "^3.6.0",
37+
"tslint-sonarts": "^1.8.0",
38+
"typescript": "^3.1.6",
39+
"webpack": "^4.25.1",
40+
"webpack-cli": "^3.1.2",
41+
"webpack-dev-server": "^3.1.10"
42+
},
43+
"dependencies": {
44+
"tslib": "^1.9.3"
45+
},
46+
"lint-staged": {
47+
"*.js": [
48+
"prettier --write",
49+
"standard --fix",
50+
"git add"
51+
],
52+
"*.@(ts|tsx)": [
53+
"prettier --write",
54+
"tslint -c tslint.json --fix",
55+
"git add"
56+
]
57+
},
58+
"prettier": {
59+
"printWidth": 120,
60+
"semi": false,
61+
"trailingComma": "all",
62+
"singleQuote": true,
63+
"arrowParens": "always",
64+
"parser": "typescript"
65+
},
66+
"husky": {
67+
"hooks": {
68+
"pre-commit": "lint-staged"
69+
}
70+
}
71+
}

playground/index.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { useState } from 'react'
2+
import ReactDOM from 'react-dom'
3+
import { interval, Observable, timer } from 'rxjs'
4+
import { exhaustMap, mapTo, scan, switchMap } from 'rxjs/operators'
5+
6+
import { useObservable } from '../src/use-observable'
7+
import { useEventCallback } from '../src/use-event-callback'
8+
9+
const mockBackendRequest = (event$: Observable<React.SyntheticEvent<HTMLHeadElement>>) =>
10+
event$.pipe(
11+
exhaustMap(() => timer(1000).pipe(mapTo(100))),
12+
scan((acc, cur) => acc + cur, 0),
13+
)
14+
15+
function IntervalValue(props: { interval: number }) {
16+
const [clickCallback, value] = useEventCallback<HTMLHeadingElement, number>(mockBackendRequest, 0)
17+
const intervalValue = useObservable(
18+
(props$) =>
19+
props$.pipe(
20+
switchMap(([intervalTime]) => interval(intervalTime)),
21+
scan((acc) => acc + 1, 0),
22+
),
23+
0,
24+
[props.interval],
25+
)
26+
return (
27+
<h1 onClick={clickCallback}>
28+
value:
29+
{value + intervalValue}
30+
</h1>
31+
)
32+
}
33+
34+
function App() {
35+
const [intervalTime, setIntervalTime] = useState(1000)
36+
const setTime = (intervalTime: number) => () => setIntervalTime(intervalTime)
37+
return (
38+
<>
39+
<IntervalValue interval={intervalTime} />
40+
<button onClick={setTime(1000)}>1000</button>
41+
<button onClick={setTime(200)}>200</button>
42+
</>
43+
)
44+
}
45+
46+
ReactDOM.render(<App />, document.querySelector('#app'))

src/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../tsconfig.json"
3+
}

src/use-event-callback.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useEffect, useState, SyntheticEvent } from 'react'
2+
import { Observable, Subject, noop } from 'rxjs'
3+
4+
export type EventCallbackState<T, U> = [((e: SyntheticEvent<T>) => void) | typeof noop, U]
5+
6+
export type EventCallback<T, U> = (eventSource$: Observable<SyntheticEvent<T>>) => Observable<U>
7+
8+
export function useEventCallback<T, U = void>(callback: EventCallback<T, U>): EventCallbackState<T, U | null>
9+
export function useEventCallback<T, U = void>(callback: EventCallback<T, U>, initialState: U): EventCallbackState<T, U>
10+
11+
export function useEventCallback<T, U = void>(
12+
callback: EventCallback<T, U>,
13+
initialState?: U,
14+
): EventCallbackState<T, U | null> {
15+
const initialValue = typeof initialState !== 'undefined' ? initialState : null
16+
const [state, setState] = useState<EventCallbackState<T, U | null>>([noop, initialValue])
17+
useEffect(
18+
() => {
19+
const event$ = new Subject<SyntheticEvent<T>>()
20+
const clickCallback = (e: SyntheticEvent<T>) => event$.next(e)
21+
setState([clickCallback, initialValue])
22+
const value$ = callback(event$)
23+
const subscription = value$.subscribe((value) => {
24+
setState([clickCallback, value])
25+
})
26+
return () => subscription.unsubscribe()
27+
},
28+
[0], // immutable forever
29+
)
30+
31+
return state
32+
}

src/use-observable.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Observable, BehaviorSubject, Subject } from 'rxjs'
2+
import { useState, useEffect, useMemo } from 'react'
3+
4+
const PREFIX = '__SUBJECT__'
5+
6+
const propsSubjects: {
7+
[index: string]: Subject<any>
8+
} = {}
9+
10+
let subjectId = 0
11+
12+
const concatSubjectKey = (id: number) => `${PREFIX}${id}`
13+
14+
export type InputFactory<T, U = undefined> = U extends undefined
15+
? () => Observable<T>
16+
: (props$: Observable<U>) => Observable<T>
17+
18+
export function useObservable<T, U = undefined>(inputFactory: InputFactory<T, U>): T | null
19+
export function useObservable<T, U = undefined>(inputFactory: InputFactory<T, U>, initialState: T): T
20+
export function useObservable<T, U extends ReadonlyArray<any>>(
21+
inputFactory: InputFactory<T, U>,
22+
initialState: T,
23+
inputs: U,
24+
): T
25+
26+
export function useObservable<T, U extends ReadonlyArray<any> | undefined>(
27+
inputFactory: InputFactory<T, U>,
28+
initialState?: T,
29+
inputs?: U,
30+
): T | null {
31+
const [state, setState] = useState<[T | null, number]>([initialState || null, 0])
32+
if (inputs) {
33+
useMemo(
34+
() => {
35+
const props$ = propsSubjects[concatSubjectKey(state[1])]
36+
if (props$) {
37+
props$.next(inputs)
38+
}
39+
},
40+
inputs as ReadonlyArray<any>,
41+
)
42+
}
43+
useEffect(
44+
() => {
45+
const props$ = new BehaviorSubject<U>(inputs as U)
46+
const input$ = (inputFactory as (...args: any[]) => Observable<T>)(
47+
typeof inputs !== 'undefined' ? props$ : void 0,
48+
)
49+
subjectId++
50+
const subscription = input$.subscribe((value) => {
51+
setState([value, subjectId])
52+
})
53+
propsSubjects[concatSubjectKey(subjectId)] = props$
54+
return () => {
55+
subscription.unsubscribe()
56+
}
57+
},
58+
[0], // immutable forever
59+
)
60+
return state[0]
61+
}

tsconfig.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"sourceMap": true,
5+
"allowSyntheticDefaultImports": true,
6+
"downlevelIteration": true,
7+
"importHelpers": true,
8+
"moduleResolution": "node",
9+
"module": "esnext",
10+
"jsx": "react",
11+
"target": "ES5",
12+
"noUnusedLocals": true,
13+
"noUnusedParameters": true,
14+
"suppressImplicitAnyIndexErrors": true,
15+
"suppressExcessPropertyErrors": true,
16+
"forceConsistentCasingInFileNames": true,
17+
"skipLibCheck": true,
18+
"declaration": true
19+
}
20+
}

0 commit comments

Comments
 (0)