Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add useStoreEffect hook #196

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 89 additions & 1 deletion __test__/lane/lane.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference path="../index.d.ts" />
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 () => {
Expand Down Expand Up @@ -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)
})
})
})
2 changes: 2 additions & 0 deletions src/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -31,6 +32,7 @@ export default {
Actions,
AsyncState,
Context,
Effects,
Middlewares,
Setter,
State,
Expand Down
16 changes: 16 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -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<any>

type Setter = {
classSetter: ClassSetter
functionSetter: FunctionSetter
Expand Down Expand Up @@ -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
}
Expand Down
38 changes: 36 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -95,6 +95,35 @@ function useModel<S>(
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<S>(useHook: CustomModelHook<S>): LaneAPI<S>
function createStore<S>(name: string, useHook: CustomModelHook<S>): LaneAPI<S>
function createStore<S>(n: any, u?: any): LaneAPI<S> {
Expand All @@ -107,13 +136,17 @@ function createStore<S>(n: any, u?: any): LaneAPI<S> {
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) => {
// s[hash] = state
// })
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -447,6 +480,7 @@ export {
actionMiddlewares,
createStore,
useModel,
useStoreEffect,
Model,
middlewares,
Provider,
Expand Down