Skip to content

Commit 8f7f137

Browse files
committed
feat(compatible): make createStore compatible with useState + useEffect
1 parent b665616 commit 8f7f137

File tree

8 files changed

+243
-55
lines changed

8 files changed

+243
-55
lines changed

README.md

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,36 +17,25 @@ The State management library for React
1717
🐛 Debug easily on test environment
1818

1919
```tsx
20-
import { Model } from 'react-model'
20+
import { useModel, createStore } from 'react-model'
2121

2222
// define model
23-
const Todo = {
24-
state: {
25-
items: ['Install react-model', 'Read github docs', 'Build App']
26-
},
27-
actions: {
28-
add: todo => {
29-
// s is the readonly version of state
30-
// you can also return partial state here but don't need to keep immutable manually
31-
// state is the mutable state
32-
return state => {
33-
state.items.push(todo)
34-
}
35-
}
36-
}
23+
const useTodo = () => {
24+
const [items, setItems] = useModel(['Install react-model', 'Read github docs', 'Build App'])
25+
return { items, setItems }
3726
}
3827

3928
// Model Register
40-
const { useStore } = Model(Todo)
29+
const { useStore } = createStore(Todo)
4130

4231
const App = () => {
4332
return <TodoList />
4433
}
4534

4635
const TodoList = () => {
47-
const [state, actions] = useStore()
36+
const { items, setItems } = useStore()
4837
return <div>
49-
<Addon handler={actions.add} />
38+
<Addon handler={setItems} />
5039
{state.items.map((item, index) => (<Todo key={index} item={item} />))}
5140
</div>
5241
}

__test__/lane/react.spec.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/// <reference path="../index.d.ts" />
2+
import { renderHook, act } from '@testing-library/react-hooks'
3+
import { createStore, useModel } from '../../src'
4+
import { useState, useEffect } from 'react'
5+
6+
describe('compatible with useState + useEffect', () => {
7+
test('compatible with useState', async () => {
8+
let renderTimes = 0
9+
const { result } = renderHook(() => {
10+
const { useStore } = createStore(() => {
11+
const [count, setCount] = useState(1)
12+
return { count, setCount }
13+
})
14+
const { count, setCount } = useStore()
15+
renderTimes += 1
16+
return { renderTimes, count, setCount }
17+
})
18+
await act(async () => {
19+
expect(result.current.renderTimes).toEqual(1)
20+
expect(result.current.count).toBe(1)
21+
})
22+
23+
await act(async () => {
24+
await result.current.setCount(5)
25+
})
26+
27+
await act(() => {
28+
expect(renderTimes).toEqual(2)
29+
expect(result.current.count).toBe(5)
30+
})
31+
})
32+
33+
test('useEffect', async () => {
34+
let renderTimes = 0
35+
let createTimes = 0
36+
let updateTimes = 0
37+
// A <A> <B /> </A>
38+
const { result } = renderHook(() => {
39+
const [count, setCount] = useState(1)
40+
useEffect(() => {
41+
createTimes += 1
42+
}, [])
43+
useEffect(() => {
44+
updateTimes += 1
45+
}, [count])
46+
47+
renderTimes += 1
48+
return { renderTimes, count, setCount }
49+
})
50+
await act(async () => {
51+
expect(result.current.renderTimes).toEqual(1)
52+
expect(result.current.count).toBe(1)
53+
expect(createTimes).toBe(1)
54+
expect(updateTimes).toBe(1)
55+
})
56+
57+
await act(async () => {
58+
await result.current.setCount(5)
59+
})
60+
61+
await act(() => {
62+
expect(renderTimes).toEqual(2)
63+
expect(result.current.count).toBe(5)
64+
expect(createTimes).toBe(1)
65+
expect(updateTimes).toBe(2)
66+
})
67+
})
68+
69+
test('compatible with useEffect', async () => {
70+
let renderTimes = 0
71+
let createTimes = 0
72+
let updateTimes = 0
73+
// A <A> <B /> </A>
74+
const { result } = renderHook(() => {
75+
const { useStore } = createStore(() => {
76+
const [count, setCount] = useState(1)
77+
useEffect(() => {
78+
createTimes += 1
79+
}, [])
80+
useEffect(() => {
81+
updateTimes += 1
82+
}, [count])
83+
return { count, setCount }
84+
})
85+
const { count, setCount } = useStore()
86+
renderTimes += 1
87+
return { renderTimes, count, setCount }
88+
})
89+
await act(async () => {
90+
expect(result.current.renderTimes).toEqual(1)
91+
expect(result.current.count).toBe(1)
92+
expect(createTimes).toBe(1)
93+
expect(updateTimes).toBe(1)
94+
})
95+
96+
await act(async () => {
97+
await result.current.setCount(5)
98+
})
99+
100+
await act(() => {
101+
expect(renderTimes).toEqual(2)
102+
expect(result.current.count).toBe(5)
103+
expect(createTimes).toBe(1)
104+
expect(updateTimes).toBe(2)
105+
})
106+
})
107+
108+
test('createStore with useState outside FC', async () => {
109+
const useCount = () => {
110+
const [count, setCount] = useState(1)
111+
return { count, setCount }
112+
}
113+
const { useStore } = createStore(useCount)
114+
let renderTimes = 0
115+
const { result } = renderHook(() => {
116+
const { count, setCount } = useStore()
117+
renderTimes += 1
118+
return { renderTimes, count, setCount }
119+
})
120+
await act(async () => {
121+
expect(result.current.renderTimes).toEqual(1)
122+
expect(result.current.count).toBe(1)
123+
})
124+
125+
await act(async () => {
126+
await result.current.setCount(5)
127+
})
128+
129+
await act(() => {
130+
expect(renderTimes).toEqual(2)
131+
expect(result.current.count).toBe(5)
132+
})
133+
})
134+
135+
test('combine useState and useStore', async () => {
136+
const useCount = () => {
137+
// useState create local state
138+
const [count, setCount] = useState(1)
139+
// useModel create shared state
140+
const [name, setName] = useModel('Jane')
141+
return { count, setCount, name, setName }
142+
}
143+
const { useStore } = createStore(useCount)
144+
let renderTimes = 0
145+
const { result } = renderHook(() => {
146+
const { count, setCount, name, setName } = useStore()
147+
renderTimes += 1
148+
return { renderTimes, count, setCount, name, setName }
149+
})
150+
const { result: otherResult } = renderHook(() => {
151+
const { count, setCount, name } = useStore()
152+
renderTimes += 1
153+
return { renderTimes, count, setCount, name }
154+
})
155+
156+
await act(async () => {
157+
expect(result.current.renderTimes).toBe(1)
158+
expect(otherResult.current.renderTimes).toBe(2)
159+
expect(result.current.count).toBe(1)
160+
})
161+
162+
await act(() => {
163+
otherResult.current.setCount(5)
164+
})
165+
166+
await act(() => {
167+
expect(result.current.renderTimes).toEqual(1)
168+
expect(otherResult.current.renderTimes).toEqual(3)
169+
expect(otherResult.current.count).toBe(5)
170+
expect(result.current.count).toBe(1)
171+
})
172+
173+
await act(() => {
174+
result.current.setCount(50)
175+
})
176+
177+
await act(() => {
178+
expect(result.current.renderTimes).toEqual(4)
179+
expect(otherResult.current.renderTimes).toEqual(3)
180+
expect(otherResult.current.count).toBe(5)
181+
expect(result.current.count).toBe(50)
182+
})
183+
184+
await act(async () => {
185+
result.current.setName('Bob')
186+
})
187+
188+
await act(() => {
189+
expect(result.current.renderTimes).toEqual(5)
190+
expect(otherResult.current.renderTimes).toEqual(6)
191+
expect(result.current.name).toBe('Bob')
192+
expect(otherResult.current.name).toBe('Bob')
193+
})
194+
})
195+
})

jest.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,8 @@ module.exports = {
131131
// testLocationInResults: false,
132132

133133
// The glob patterns Jest uses to detect test files
134-
// testMatch: [
135-
// "**/__test__/**/lane.spec.[jt]s?(x)",
134+
// ,testMatch: [
135+
// "**/__test__/**/react.spec.[jt]s?(x)",
136136
// // "**/?(*.)+(spec|test).[tj]s?(x)"
137137
// ],
138138

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
22
"name": "react-model",
3-
"version": "4.0.2",
3+
"version": "4.1.0-alpha.1",
44
"description": "The State management library for React",
55
"main": "./dist/react-model.js",
66
"module": "./dist/react-model.esm.js",
77
"umd:main": "./dist/react-model.umd.js",
8+
"types": "./src/index",
89
"scripts": {
910
"build:prod": "microbundle --define process.env.NODE_ENV=production --sourcemap false --jsx React.createElement --output dist --tsconfig ./tsconfig.json",
1011
"build:dev": "microbundle --define process.env.NODE_ENV=development --sourcemap true --jsx React.createElement --output dist --tsconfig ./tsconfig.json",

src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ interface BaseContext<S = {}, P = any> {
8989
actionName: string
9090
modelName: string
9191
next?: Function
92+
disableSelectorUpdate?: boolean
9293
newState: Global['State'] | Function | null
9394
Global: Global
9495
}

src/index.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ const isAPI = (input: any): input is API => {
2525
// DON'T USE useModel OUTSIDE createStore func
2626
function useModel<S>(state: S): [S, (state: S) => void] {
2727
const storeId = Global.currentStoreId
28-
if (!Global.mutableState[storeId]) {
29-
Global.mutableState[storeId] = { count: 0 }
30-
}
3128
const index = Global.mutableState[storeId].count
3229
Global.mutableState[storeId].count += 1
3330
if (!Global.mutableState[storeId][index]) {
@@ -42,6 +39,7 @@ function useModel<S>(state: S): [S, (state: S) => void] {
4239
},
4340
actionName: 'setter',
4441
consumerActions,
42+
disableSelectorUpdate: true,
4543
middlewareConfig: {},
4644
modelName: '__' + storeId,
4745
newState: {},
@@ -61,11 +59,14 @@ function createStore<S>(useHook: CustomModelHook<S>): LaneAPI<S> {
6159
if (!Global.Actions[hash]) {
6260
Global.Actions[hash] = {}
6361
}
64-
Global.currentStoreId = storeId
65-
const state = useHook()
66-
Global.State = produce(Global.State, (s) => {
67-
s[hash] = state
68-
})
62+
if (!Global.mutableState[storeId]) {
63+
Global.mutableState[storeId] = { count: 0 }
64+
}
65+
// Global.currentStoreId = storeId
66+
// const state = useHook()
67+
// Global.State = produce(Global.State, (s) => {
68+
// s[hash] = state
69+
// })
6970
const selector = () => {
7071
Global.mutableState[storeId].count = 0
7172
Global.currentStoreId = storeId

src/middlewares.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,32 +34,33 @@ const getNewState: Middleware = async (context, restMiddlewares) => {
3434
return await next(restMiddlewares)
3535
}
3636

37-
const getNewStateWithCache =
38-
(maxTime: number = 5000): Middleware =>
39-
async (context, restMiddlewares) => {
40-
const {
41-
action,
42-
Global,
43-
modelName,
44-
consumerActions,
45-
params,
46-
next,
47-
actionName
48-
} = context
49-
context.newState =
50-
(await Promise.race([
51-
action(params, {
52-
actions: consumerActions(Global.Actions[modelName], { modelName }),
53-
state: Global.State[modelName]
54-
}),
55-
timeout(maxTime, getCache(modelName, actionName))
56-
])) || null
57-
return await next(restMiddlewares)
58-
}
37+
const getNewStateWithCache = (maxTime: number = 5000): Middleware => async (
38+
context,
39+
restMiddlewares
40+
) => {
41+
const {
42+
action,
43+
Global,
44+
modelName,
45+
consumerActions,
46+
params,
47+
next,
48+
actionName
49+
} = context
50+
context.newState =
51+
(await Promise.race([
52+
action(params, {
53+
actions: consumerActions(Global.Actions[modelName], { modelName }),
54+
state: Global.State[modelName]
55+
}),
56+
timeout(maxTime, getCache(modelName, actionName))
57+
])) || null
58+
return await next(restMiddlewares)
59+
}
5960

6061
const setNewState: Middleware = async (context, restMiddlewares) => {
61-
const { modelName, newState, next, Global } = context
62-
if (Global.Setter.functionSetter[modelName]) {
62+
const { modelName, newState, next, Global, disableSelectorUpdate } = context
63+
if (Global.Setter.functionSetter[modelName] && !disableSelectorUpdate) {
6364
Object.keys(Global.Setter.functionSetter[modelName]).map((key) => {
6465
const setter = Global.Setter.functionSetter[modelName][key]
6566
if (setter) {
@@ -153,15 +154,15 @@ const devToolsListener: Middleware = async (context, restMiddlewares) => {
153154
}
154155

155156
const communicator: Middleware = async (context, restMiddlewares) => {
156-
const { modelName, next, Global } = context
157+
const { modelName, next, Global, disableSelectorUpdate } = context
157158
if (Global.Setter.classSetter) {
158159
Global.Setter.classSetter(Global.State)
159160
}
160161
if (Global.Setter.functionSetter[modelName]) {
161162
Object.keys(Global.Setter.functionSetter[modelName]).map((key) => {
162163
const setter = Global.Setter.functionSetter[modelName][key]
163164
if (setter) {
164-
if (!setter.selector) {
165+
if (!setter.selector || disableSelectorUpdate) {
165166
setter.setState(Global.State[modelName])
166167
} else {
167168
const newSelectorRef = setter.selector(Global.State[modelName])

0 commit comments

Comments
 (0)