diff --git a/example/index.tsx b/example/index.tsx index 8c01a0c..c94a498 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -1,8 +1,10 @@ // @ts-ignore import React, { ChangeEvent, useContext, useEffect } from "react" import { + createReactive, createSubscription, ISubscription, + useReactive, useReducerSubscription, useSubscription, } from "../src/index" @@ -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 state.text1 = e.target.value} /> +} +const Text2 = () => { + const state = useReactive(sourceOfTruth, ['text2']) + return state.text2 = e.target.value} /> +} + +const ReactiveApp = () => { + + return
+

Reactive pattern:

+ + <> + + + + +
+} + function App() { return ( <> @@ -159,6 +189,7 @@ function App() { + ) } diff --git a/package.json b/package.json index f4a788c..b3e49d0 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" } } diff --git a/src/index.ts b/src/index.ts index 143149d..47de31e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,29 +3,49 @@ import React from "react" type Listener = (newState: S) => void export interface ISubscription { + __updateState?: (nextState: any, forceUpdate?: boolean) => void subscribe: (fn: Listener) => void unsubscribe: (fn: Listener) => void listener: Set> state: S - updateState: (nextState: S, forceUpdate?: boolean) => void + updateState: (nextState: S | any, forceUpdate?: boolean) => void + [key: string]: any +} +export interface IReactive { + subscribe: (fn: Listener) => void + unsubscribe: (fn: Listener) => void + listener: Set> + store: S } -export function createSubscription( - initialState?: S, +export function createSubscription( + initialState?: S ): ISubscription { - const state: S = initialState || ({} as any) + let state: S = initialState || ({} as any) let listener: Set> = new Set() const subscribe = (fn: Listener) => { listener.add(fn) } - const unsubscribe = (fn: Listener) => listener.delete(fn) + const unsubscribe = (fn: Listener) => + 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 { @@ -41,29 +61,27 @@ export interface IStateReduceUpdater { function useSubscriber( subscriber: ISubscription, - 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 @@ -71,43 +89,83 @@ function useSubscriber( export function useReducerSubscription( subscriber: ISubscription, - reducer: any = () => {}, + reducer: any = () => { + } ): IStateReduceUpdater { 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( subscriber: ISubscription, - pick?: string[], + pick?: string[] ): IStateUpdater { 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 = (initialState: S): IReactive => { + let listener: Set> = new Set() + const subscribe = (fn: Listener) => listener.add(fn) + const unsubscribe = (fn: Listener) => 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 = (reactiveStore: IReactive, pick?: Array) => { + 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 +}; +