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,