diff --git a/__test__/lane/lane.spec.ts b/__test__/lane/lane.spec.ts index 58847f6..e4e1ef4 100644 --- a/__test__/lane/lane.spec.ts +++ b/__test__/lane/lane.spec.ts @@ -1,6 +1,6 @@ /// import { renderHook, act } from '@testing-library/react-hooks' -import { createStore, useModel, Model } from '../../src' +import { createStore, useModel, Model, useStoreEffect } from '../../src' describe('lane model', () => { test('single model', async () => { @@ -433,4 +433,92 @@ describe('lane model', () => { expect(getState().count).toBe(1) }) }) + + test('complex case with useStoreEffect', async () => { + let onceEffectInvokeTimes = 0 + let nameEffectInvokeTimes = 0 + let countEffectInvokeTimes = 0 + + const { useStore } = createStore(() => { + const [count, setCount] = useModel(1) + const [name, setName] = useModel('Jane') + useStoreEffect(() => { + onceEffectInvokeTimes += 1 + }, []) + useStoreEffect(() => { + nameEffectInvokeTimes += 1 + }, [name]) + useStoreEffect(() => { + countEffectInvokeTimes += 1 + }, [count]) + + return { count, setCount, name, setName } + }) + + let renderTimes = 0 + const { result } = renderHook(() => { + const { count, setCount, setName } = useStore() + renderTimes += 1 + return { renderTimes, count, setCount, setName } + }) + + const { result: mirrorResult } = renderHook(() => { + const { setName, name } = useStore() + renderTimes += 1 + return { renderTimes, setName, name } + }) + + act(() => { + expect(renderTimes).toEqual(2) + expect(mirrorResult.current.name).toBe('Jane') + expect(onceEffectInvokeTimes).toEqual(1) + expect(nameEffectInvokeTimes).toEqual(1) + expect(countEffectInvokeTimes).toEqual(1) + }) + + act(() => { + mirrorResult.current.setName('Bob') + }) + + act(() => { + expect(renderTimes).toEqual(4) + expect(onceEffectInvokeTimes).toEqual(1) + expect(nameEffectInvokeTimes).toEqual(2) + expect(countEffectInvokeTimes).toEqual(1) + expect(mirrorResult.current.name).toBe('Bob') + }) + + act(() => { + result.current.setName('Jane') + }) + + act(() => { + expect(mirrorResult.current.name).toBe('Jane') + expect(onceEffectInvokeTimes).toEqual(1) + expect(nameEffectInvokeTimes).toEqual(3) + expect(countEffectInvokeTimes).toEqual(1) + }) + + act(() => { + result.current.setCount(2) + }) + + act(() => { + expect(result.current.count).toBe(2) + expect(onceEffectInvokeTimes).toEqual(1) + expect(nameEffectInvokeTimes).toEqual(3) + expect(countEffectInvokeTimes).toEqual(2) + }) + + act(() => { + result.current.setCount(2) + }) + + act(() => { + expect(mirrorResult.current.name).toBe('Jane') + expect(onceEffectInvokeTimes).toEqual(1) + expect(nameEffectInvokeTimes).toEqual(3) + expect(countEffectInvokeTimes).toEqual(2) + }) + }) }) diff --git a/src/global.ts b/src/global.ts index 240ff8d..a2aa0bc 100644 --- a/src/global.ts +++ b/src/global.ts @@ -2,6 +2,7 @@ const State = {} const mutableState = {} const Actions = {} const AsyncState = {} +const Effects = {} const Middlewares = {} // Communicate between Provider-Consumer and Hooks const Setter: Setter = { @@ -31,6 +32,7 @@ export default { Actions, AsyncState, Context, + Effects, Middlewares, Setter, State, diff --git a/src/index.d.ts b/src/index.d.ts index 7854985..4d7c9f4 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,3 +1,9 @@ +declare const UNDEFINED_VOID_ONLY: unique symbol +// Destructors are only allowed to return void. +type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never } +type EffectCallback = () => void | Destructor +type DependencyList = ReadonlyArray + type Setter = { classSetter: ClassSetter functionSetter: FunctionSetter @@ -28,6 +34,16 @@ interface Global { State: { [modelName: string]: any } + Effects: { + [modelName: string]: { + idx: number + effects: Array<{ + effectCallback: EffectCallback + deps: DependencyList + destructor: Destructor | void + }> + } + } mutableState: { [modelName: string]: any } diff --git a/src/index.tsx b/src/index.tsx index 15f3ce3..9a55fac 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -20,7 +20,7 @@ import { } from './helper' import { actionMiddlewares, applyMiddlewares, middlewares } from './middlewares' -const useStoreEffect = +const useEffectImpl = typeof window === 'undefined' ? useEffect : useLayoutEffect const isModelType = (input: any): input is ModelType => { @@ -95,6 +95,35 @@ function useModel( return [Global.mutableState[storeId][index], setter] } +function useStoreEffect(effect: EffectCallback, deps: DependencyList) { + const storeId = Global.currentStoreId + const index = Global.Effects[storeId].idx + Global.Effects[storeId].idx += 1 + if (!Global.Effects[storeId].effects[index]) { + // mount effect + const destructor = effect() + Global.Effects[storeId].effects[index] = { + effectCallback: effect, + deps, + destructor + } + } else { + let isDepChanged = false + const preDeps = Global.Effects[storeId].effects[index].deps + const destructor = Global.Effects[storeId].effects[index].destructor + deps.forEach((dep, idx) => { + if (preDeps[idx] !== dep) isDepChanged = true + }) + if (isDepChanged) { + if (destructor) { + destructor() + } + Global.Effects[storeId].effects[index].deps = deps + Global.Effects[storeId].effects[index].destructor = effect() + } + } +} + function createStore(useHook: CustomModelHook): LaneAPI function createStore(name: string, useHook: CustomModelHook): LaneAPI function createStore(n: any, u?: any): LaneAPI { @@ -107,6 +136,9 @@ function createStore(n: any, u?: any): LaneAPI { if (!Global.mutableState[storeId]) { Global.mutableState[storeId] = { count: 0 } } + if (!Global.Effects[storeId]) { + Global.Effects[storeId] = { idx: 0, effects: [] } + } // Global.currentStoreId = storeId // const state = useHook() // Global.State = produce(Global.State, (s) => { @@ -114,6 +146,7 @@ function createStore(n: any, u?: any): LaneAPI { // }) const selector = () => { Global.mutableState[storeId].count = 0 + Global.Effects[storeId].idx = 0 Global.currentStoreId = storeId Global.mutableState[storeId].cachedResult = u ? u() : n() return Global.mutableState[storeId].cachedResult @@ -365,7 +398,7 @@ const useStore = (modelName: string, selector?: Function) => { const usedSelector = isFromCreateStore ? mutableState.selector : selector const usedState = isFromCreateStore ? mutableState : getState(modelName) - useStoreEffect(() => { + useEffectImpl(() => { Global.uid += 1 const local_hash = '' + Global.uid hash.current = local_hash @@ -447,6 +480,7 @@ export { actionMiddlewares, createStore, useModel, + useStoreEffect, Model, middlewares, Provider,