diff --git a/package.json b/package.json index d25bfb7..64cfeba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-tivity", - "version": "1.0.0-beta.0", + "version": "1.0.0-beta.1", "description": "State solution for React", "private": true, "main": "./dist/index.js", diff --git a/src/__tests__/create-test.tsx b/src/__tests__/create-test.tsx index d395596..09d53cc 100644 --- a/src/__tests__/create-test.tsx +++ b/src/__tests__/create-test.tsx @@ -179,3 +179,44 @@ describe('create tests', () => { expect(cb).toHaveBeenCalledTimes(1) }) }) + +test('Sets state asynchronously', async () => { + type State = { + values: any + setValues: (state: State) => void + } + + const requestData = () => + new Promise(resolve => { + setTimeout(() => resolve([{ value: 'foo' }, { value: 'bar' }]), 1000) + }) + + const useHook = create({ + values: [], + setValues: async state => { + let res = await requestData() + state.values = await res + } + }) + + function Component() { + let { values, setValues } = useHook() + + return ( +
+
+ {values.length ? `${values[0].value}&${values[1].value}` : 'no value'} +
+ +
+ ) + } + + let { findByText, getByText } = render() + + await findByText('no value') + + fireEvent.click(getByText('change')) + + await findByText('foo&bar') +}) diff --git a/src/__tests__/unit/storage-test.ts b/src/__tests__/unit/storage-test.ts index 36f5473..ed46974 100644 --- a/src/__tests__/unit/storage-test.ts +++ b/src/__tests__/unit/storage-test.ts @@ -18,4 +18,4 @@ describe('Internal storage tests', () => { '[react-tivity] window undefined failed to build localStorage falling back to noopStorage' ) }) -}) \ No newline at end of file +}) diff --git a/src/apis/create.ts b/src/apis/create.ts index 68fb3c7..da8da5f 100644 --- a/src/apis/create.ts +++ b/src/apis/create.ts @@ -1,13 +1,13 @@ import { initStore, useStore } from '../utils' import type { Obj, State, CreateHook } from '../utils' -export function create( - arg: StateObj | (() => StateObj) -): CreateHook { +export function create( + arg: TState | (() => TState) +): CreateHook { const initObj = typeof arg === 'function' ? arg() : arg - const store = initStore>(initObj) + const store = initStore>(initObj) - const hook = () => useStore(store) + const hook = () => useStore>(store) const useHook = Object.assign(hook, { subscribe: store.subscribe, diff --git a/src/apis/persist.ts b/src/apis/persist.ts index c521f5e..026ebff 100644 --- a/src/apis/persist.ts +++ b/src/apis/persist.ts @@ -116,7 +116,7 @@ export function persist( store.setStateImpl({ ...toSet, _status: true }) }) - store.subscribe(saveToStorage) + store.subscribe(() => saveToStorage()) const hook = () => useStore< diff --git a/src/utils/initStore.ts b/src/utils/initStore.ts index e8f061f..6d7e284 100644 --- a/src/utils/initStore.ts +++ b/src/utils/initStore.ts @@ -1,11 +1,13 @@ import type { Obj } from './types' export function initStore(initObj: Obj) { - const subscribers = new Set<() => void | any>() + const subscribers = new Set<(prev: Obj, next: Obj) => void | any>() + const copyObj = (obj: Obj = getSnapshot()) => JSON.parse(JSON.stringify(obj)) const setStateImpl = (nextState: Obj) => { - state = Object.assign({}, state, nextState) - subscribers.forEach(cb => cb()) + state = Object.assign({}, state, copyObj(nextState)) + subscribers.forEach(cb => cb(prevState, state)) + prevState = state } const setState = (method: any, args: any) => method(proxiedState, ...args) @@ -21,6 +23,7 @@ export function initStore(initObj: Obj) { } }) + let prevState = state const getSnapshot = () => state const subscribe = (cb: () => void | any) => { @@ -29,7 +32,7 @@ export function initStore(initObj: Obj) { } const proxiedState = ((): State => { - let stateCopy = JSON.parse(JSON.stringify(getSnapshot())) + let stateCopy = copyObj() Object.keys(state).forEach(key => { if (typeof state[key] === 'function') { @@ -49,8 +52,7 @@ export function initStore(initObj: Obj) { }, set(obj: Obj, prop: string, value: any) { obj[prop] = value - if (Object.keys(obj).some(key => typeof obj[key] === 'function')) - setStateImpl(obj) + setStateImpl(stateCopy) return true } } diff --git a/src/utils/uSES.ts b/src/utils/uSES.ts index 7637d6f..b878e34 100644 --- a/src/utils/uSES.ts +++ b/src/utils/uSES.ts @@ -1,54 +1,58 @@ -import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector.js' +import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js' import type { Obj } from './types' -import { useRef } from 'react' +import { useRef, useMemo } from 'react' -const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports +const isObject = (x: any) => { + return x ? (typeof x === 'object' ? true : false) : false +} -export function useStore(store: any) { - const stateDepRefs = useRef([]) +const isDeepEqual = (a: any, b: any) => { + const sameType = typeof a === typeof b ? true : false + let equal = true + if (sameType && isObject(b)) { + const bKeys = Object.keys(b) + + bKeys.forEach(key => { + if (!isObject(b[key])) { + if (a[key] !== b[key]) equal = false + } else { + if (!isDeepEqual(a[key], b[key])) equal = false + } + }) - const isObject = (x: any) => { - return x ? (typeof x === 'object' ? true : false) : false + return equal } + return a === b +} - const isEqual = (a: any, b: any) => { - const sameType = typeof a === typeof b ? true : false - let equal = true - if (sameType && isObject(a)) { - const aKeys = Object.keys(a) - - aKeys.forEach(key => { - if (!isObject(a[key])) { - if (a[key] !== b[key]) equal = false - } else { - if (!isEqual(a[key], b[key])) equal = false - } - }) +export function useStore(store: any): TState { + const stateDepRefs = useRef([]) - return equal - } - return a === b - } + const isEqual = (prev: Obj, next: Obj) => + stateDepRefs.current.every( + slice => isDeepEqual(prev[slice], next[slice]) === true + ) - const state = useSyncExternalStoreWithSelector( - store.subscribe, - store.getSnapshot, - store.getSnapshot, - (s: TState) => s, - (prev: Obj, next: Obj) => { - for (let slice in prev) { - if (stateDepRefs.current.includes(slice)) { - if (!isEqual(prev[slice], next[slice])) { - return false - } - } - } - return true + const getSnapshot = useMemo(() => { + let memoized = store.getSnapshot() + return () => { + let current = store.getSnapshot() + return Object.is(memoized, current) ? memoized : (memoized = current) } + }, [store.getSnapshot()]) + + const state = useSyncExternalStore( + cb => { + return store.subscribe((prev: Obj, next: Obj) => { + if (!isEqual(prev, next)) cb() + }) + }, + getSnapshot, + getSnapshot ) const handler = { - get(obj: TState, prop: string) { + get(obj: Obj, prop: string) { if ( !stateDepRefs.current.includes(prop) && typeof obj[prop] !== 'function'