Skip to content

Commit

Permalink
Add Reactive pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-phan committed May 27, 2021
1 parent 37dbc2e commit 960fe30
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 59 deletions.
31 changes: 31 additions & 0 deletions example/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// @ts-ignore
import React, { ChangeEvent, useContext, useEffect } from "react"
import {
createReactive,
createSubscription,
ISubscription,
useReactive,
useReducerSubscription,
useSubscription,
} from "../src/index"
Expand Down Expand Up @@ -150,6 +152,34 @@ function Counter2() {
)
}


const sourceOfTruth = createReactive({
text1: 'Text 1 sync together',
text2: 'Text 2 walk alone.'
})

const Text1 = () => {
const state = useReactive(sourceOfTruth, ['text1'])
return <input value={state.text1} onChange={e => state.text1 = e.target.value} />
}
const Text2 = () => {
const state = useReactive(sourceOfTruth, ['text2'])
return <input value={state.text2} onChange={e => state.text2 = e.target.value} />
}

const ReactiveApp = () => {

return <div>
<h1>Reactive pattern:</h1>

<>
<Text1 />
<Text2/>
<Text1 />
</>
</div>
}

function App() {
return (
<>
Expand All @@ -159,6 +189,7 @@ function App() {
<FooDisplay />
<TextComponent />
<MountAndUnmount />
<ReactiveApp />
</>
)
}
Expand Down
31 changes: 9 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "global-state-hook",
"version": "1.6.0",
"version": "2.0.0",
"description": "Super tiny state sharing library with hooks",
"source": "src/index.ts",
"main": "dist/index.js",
Expand All @@ -25,27 +25,14 @@
},
"license": "MIT",
"devDependencies": {
"@types/react": "^16.9.48",
"@types/react-dom": "^16.9.8",
"husky": "^4.2.5",
"lint-staged": "^10.2.13",
"microbundle": "^0.12.3",
"@types/react": "^16.14.8",
"@types/react-dom": "^16.9.13",
"microbundle": "^0.13.1",
"parcel": "^1.12.4",
"prettier": "^2.1.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"ts-node": "^9.0.0",
"typescript": "^4.0.2"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"git add"
]
"prettier": "^2.3.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"ts-node": "^9.1.1",
"typescript": "^4.3.2"
}
}
132 changes: 95 additions & 37 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,49 @@ import React from "react"
type Listener<S> = (newState: S) => void

export interface ISubscription<S extends any> {
__updateState?: (nextState: any, forceUpdate?: boolean) => void
subscribe: (fn: Listener<S>) => void
unsubscribe: (fn: Listener<S>) => void
listener: Set<Listener<S>>
state: S
updateState: (nextState: S, forceUpdate?: boolean) => void
updateState: (nextState: S | any, forceUpdate?: boolean) => void
[key: string]: any
}
export interface IReactive<S extends any> {
subscribe: (fn: Listener<string>) => void
unsubscribe: (fn: Listener<string>) => void
listener: Set<Listener<string>>
store: S
}

export function createSubscription<S extends any>(
initialState?: S,
export function createSubscription<S extends object>(
initialState?: S
): ISubscription<S> {
const state: S = initialState || ({} as any)
let state: S = initialState || ({} as any)
let listener: Set<Listener<S>> = new Set()
const subscribe = (fn: Listener<S>) => {
listener.add(fn)
}
const unsubscribe = (fn: Listener<S>) => listener.delete(fn)
const unsubscribe = (fn: Listener<S>) =>
listener.delete(fn)
const updateState = (nextState: S, forceUpdate = true) => {
Object.assign(state, nextState)
state = { ...state, ...nextState }
if (forceUpdate) {
listener.forEach((fn) => fn(nextState))
listener.forEach(fn => fn(nextState))
}
}
return { subscribe, unsubscribe, listener, state, updateState }
return {
subscribe,
unsubscribe,
listener,
get state() {
return state
},
set state(nextState) {
state = nextState
},
updateState
}
}

export interface IStateUpdater<S> {
Expand All @@ -41,73 +61,111 @@ export interface IStateReduceUpdater<S> {

function useSubscriber<S extends object>(
subscriber: ISubscription<S>,
pick?: string[],
pick?: string[]
) {
if (!subscriber) {
console.trace('Missing subscriber!!')
}
const [changed, setUpdate] = React.useState({})
// const mounted = React.useRef(true)
const updater = React.useCallback(
(nextState: S) => {
if (
!pick ||
const updater = React.useCallback((nextState: S) => {
if (subscriber &&
(!pick ||
!pick.length ||
typeof nextState !== "object" ||
nextState.constructor !== Object ||
Object.keys(nextState).find((k) => pick.includes(k))
) {
setUpdate({})
}
},
[pick],
)
Object.keys(nextState).find((k) => pick.includes(k)))
) {
setUpdate({})
}
}, [pick])
React.useEffect(() => {
subscriber.subscribe(updater)
return () => {
subscriber.unsubscribe(updater)
if (subscriber) {
subscriber.subscribe(updater)
return () => subscriber.unsubscribe(updater)
}
return
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return changed
}

export function useReducerSubscription<S extends object>(
subscriber: ISubscription<S>,
reducer: any = () => {},
reducer: any = () => {
}
): IStateReduceUpdater<S> {
useSubscriber(subscriber)
const dispatch = (...args: any) => {
const newState = reducer(subscriber.state, ...args)
subscriber.state = Object.assign({}, subscriber.state, newState)
subscriber.listener.forEach((fn) => fn(newState))
subscriber.listener.forEach(fn => fn(newState))
}
React.useDebugValue(subscriber.state)

return { state: subscriber.state, dispatch }
return { state: subscriber?.state, dispatch }
}

export function useSubscription<S extends object>(
subscriber: ISubscription<S>,
pick?: string[],
pick?: string[]
): IStateUpdater<S> {
const changed = useSubscriber(subscriber, pick)
React.useDebugValue(subscriber.state)
React.useDebugValue(subscriber?.state)

return {
changed,
state: subscriber.state,
state: subscriber?.state,
setState: React.useCallback(
(newState: any, callback?: Function) => {
if (typeof newState === "object" && newState.constructor === Object) {
if (!subscriber) {
return
}
if (typeof newState === 'object' && newState.constructor === Object) {
subscriber.state = Object.assign({}, subscriber.state, newState)
} else if (typeof newState === "function") {
} else if (typeof newState === 'function') {
const nextState = newState(subscriber.state)
subscriber.state = Object.assign({}, subscriber.state, nextState)
} else {
subscriber.state = newState
}
subscriber.listener.forEach((fn) => fn(newState))
subscriber.listener.forEach(fn => fn(newState))
callback && callback()
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[subscriber.state, pick],
),
[subscriber.state, pick]
)
}
}


export const createReactive = <S extends object>(initialState: S): IReactive<S> => {
let listener: Set<Listener<string>> = new Set()
const subscribe = (fn: Listener<string>) => listener.add(fn)
const unsubscribe = (fn: Listener<string>) => listener.delete(fn)
const store: S = new Proxy(initialState, {
set(target, p: string, value, receiver) {
listener.forEach((fn) => fn(p))
return Reflect.set(target, p, value, receiver)
}
})
return {
listener, store, subscribe, unsubscribe
}
}

export const useReactive = <S extends object>(reactiveStore: IReactive<S>, pick?: Array<string>) => {
const [, setUpdate] = React.useState({})
const updater = React.useCallback((prop) => {
if (!pick || (Array.isArray(pick) && pick.includes(prop))) {
setUpdate({})
}
}, [pick])
React.useEffect(() => {
if (reactiveStore) {
reactiveStore.subscribe(updater)
return () => reactiveStore.unsubscribe(updater)
}
return
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return reactiveStore.store
};

0 comments on commit 960fe30

Please sign in to comment.