Skip to content

Commit

Permalink
feat(compatible): make createStore compatible with useState + useEffect
Browse files Browse the repository at this point in the history
  • Loading branch information
ArrayZoneYour committed Aug 3, 2021
1 parent b665616 commit 8f7f137
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 55 deletions.
25 changes: 7 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,25 @@ The State management library for React
🐛 Debug easily on test environment

```tsx
import { Model } from 'react-model'
import { useModel, createStore } from 'react-model'

// define model
const Todo = {
state: {
items: ['Install react-model', 'Read github docs', 'Build App']
},
actions: {
add: todo => {
// s is the readonly version of state
// you can also return partial state here but don't need to keep immutable manually
// state is the mutable state
return state => {
state.items.push(todo)
}
}
}
const useTodo = () => {
const [items, setItems] = useModel(['Install react-model', 'Read github docs', 'Build App'])
return { items, setItems }
}

// Model Register
const { useStore } = Model(Todo)
const { useStore } = createStore(Todo)

const App = () => {
return <TodoList />
}

const TodoList = () => {
const [state, actions] = useStore()
const { items, setItems } = useStore()
return <div>
<Addon handler={actions.add} />
<Addon handler={setItems} />
{state.items.map((item, index) => (<Todo key={index} item={item} />))}
</div>
}
Expand Down
File renamed without changes.
195 changes: 195 additions & 0 deletions __test__/lane/react.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/// <reference path="../index.d.ts" />
import { renderHook, act } from '@testing-library/react-hooks'
import { createStore, useModel } from '../../src'
import { useState, useEffect } from 'react'

describe('compatible with useState + useEffect', () => {
test('compatible with useState', async () => {
let renderTimes = 0
const { result } = renderHook(() => {
const { useStore } = createStore(() => {
const [count, setCount] = useState(1)
return { count, setCount }
})
const { count, setCount } = useStore()
renderTimes += 1
return { renderTimes, count, setCount }
})
await act(async () => {
expect(result.current.renderTimes).toEqual(1)
expect(result.current.count).toBe(1)
})

await act(async () => {
await result.current.setCount(5)
})

await act(() => {
expect(renderTimes).toEqual(2)
expect(result.current.count).toBe(5)
})
})

test('useEffect', async () => {
let renderTimes = 0
let createTimes = 0
let updateTimes = 0
// A <A> <B /> </A>
const { result } = renderHook(() => {
const [count, setCount] = useState(1)
useEffect(() => {
createTimes += 1
}, [])
useEffect(() => {
updateTimes += 1
}, [count])

renderTimes += 1
return { renderTimes, count, setCount }
})
await act(async () => {
expect(result.current.renderTimes).toEqual(1)
expect(result.current.count).toBe(1)
expect(createTimes).toBe(1)
expect(updateTimes).toBe(1)
})

await act(async () => {
await result.current.setCount(5)
})

await act(() => {
expect(renderTimes).toEqual(2)
expect(result.current.count).toBe(5)
expect(createTimes).toBe(1)
expect(updateTimes).toBe(2)
})
})

test('compatible with useEffect', async () => {
let renderTimes = 0
let createTimes = 0
let updateTimes = 0
// A <A> <B /> </A>
const { result } = renderHook(() => {
const { useStore } = createStore(() => {
const [count, setCount] = useState(1)
useEffect(() => {
createTimes += 1
}, [])
useEffect(() => {
updateTimes += 1
}, [count])
return { count, setCount }
})
const { count, setCount } = useStore()
renderTimes += 1
return { renderTimes, count, setCount }
})
await act(async () => {
expect(result.current.renderTimes).toEqual(1)
expect(result.current.count).toBe(1)
expect(createTimes).toBe(1)
expect(updateTimes).toBe(1)
})

await act(async () => {
await result.current.setCount(5)
})

await act(() => {
expect(renderTimes).toEqual(2)
expect(result.current.count).toBe(5)
expect(createTimes).toBe(1)
expect(updateTimes).toBe(2)
})
})

test('createStore with useState outside FC', async () => {
const useCount = () => {
const [count, setCount] = useState(1)
return { count, setCount }
}
const { useStore } = createStore(useCount)
let renderTimes = 0
const { result } = renderHook(() => {
const { count, setCount } = useStore()
renderTimes += 1
return { renderTimes, count, setCount }
})
await act(async () => {
expect(result.current.renderTimes).toEqual(1)
expect(result.current.count).toBe(1)
})

await act(async () => {
await result.current.setCount(5)
})

await act(() => {
expect(renderTimes).toEqual(2)
expect(result.current.count).toBe(5)
})
})

test('combine useState and useStore', async () => {
const useCount = () => {
// useState create local state
const [count, setCount] = useState(1)
// useModel create shared state
const [name, setName] = useModel('Jane')
return { count, setCount, name, setName }
}
const { useStore } = createStore(useCount)
let renderTimes = 0
const { result } = renderHook(() => {
const { count, setCount, name, setName } = useStore()
renderTimes += 1
return { renderTimes, count, setCount, name, setName }
})
const { result: otherResult } = renderHook(() => {
const { count, setCount, name } = useStore()
renderTimes += 1
return { renderTimes, count, setCount, name }
})

await act(async () => {
expect(result.current.renderTimes).toBe(1)
expect(otherResult.current.renderTimes).toBe(2)
expect(result.current.count).toBe(1)
})

await act(() => {
otherResult.current.setCount(5)
})

await act(() => {
expect(result.current.renderTimes).toEqual(1)
expect(otherResult.current.renderTimes).toEqual(3)
expect(otherResult.current.count).toBe(5)
expect(result.current.count).toBe(1)
})

await act(() => {
result.current.setCount(50)
})

await act(() => {
expect(result.current.renderTimes).toEqual(4)
expect(otherResult.current.renderTimes).toEqual(3)
expect(otherResult.current.count).toBe(5)
expect(result.current.count).toBe(50)
})

await act(async () => {
result.current.setName('Bob')
})

await act(() => {
expect(result.current.renderTimes).toEqual(5)
expect(otherResult.current.renderTimes).toEqual(6)
expect(result.current.name).toBe('Bob')
expect(otherResult.current.name).toBe('Bob')
})
})
})
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ module.exports = {
// testLocationInResults: false,

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

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"name": "react-model",
"version": "4.0.2",
"version": "4.1.0-alpha.1",
"description": "The State management library for React",
"main": "./dist/react-model.js",
"module": "./dist/react-model.esm.js",
"umd:main": "./dist/react-model.umd.js",
"types": "./src/index",
"scripts": {
"build:prod": "microbundle --define process.env.NODE_ENV=production --sourcemap false --jsx React.createElement --output dist --tsconfig ./tsconfig.json",
"build:dev": "microbundle --define process.env.NODE_ENV=development --sourcemap true --jsx React.createElement --output dist --tsconfig ./tsconfig.json",
Expand Down
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ interface BaseContext<S = {}, P = any> {
actionName: string
modelName: string
next?: Function
disableSelectorUpdate?: boolean
newState: Global['State'] | Function | null
Global: Global
}
Expand Down
17 changes: 9 additions & 8 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ const isAPI = (input: any): input is API => {
// DON'T USE useModel OUTSIDE createStore func
function useModel<S>(state: S): [S, (state: S) => void] {
const storeId = Global.currentStoreId
if (!Global.mutableState[storeId]) {
Global.mutableState[storeId] = { count: 0 }
}
const index = Global.mutableState[storeId].count
Global.mutableState[storeId].count += 1
if (!Global.mutableState[storeId][index]) {
Expand All @@ -42,6 +39,7 @@ function useModel<S>(state: S): [S, (state: S) => void] {
},
actionName: 'setter',
consumerActions,
disableSelectorUpdate: true,
middlewareConfig: {},
modelName: '__' + storeId,
newState: {},
Expand All @@ -61,11 +59,14 @@ function createStore<S>(useHook: CustomModelHook<S>): LaneAPI<S> {
if (!Global.Actions[hash]) {
Global.Actions[hash] = {}
}
Global.currentStoreId = storeId
const state = useHook()
Global.State = produce(Global.State, (s) => {
s[hash] = state
})
if (!Global.mutableState[storeId]) {
Global.mutableState[storeId] = { count: 0 }
}
// Global.currentStoreId = storeId
// const state = useHook()
// Global.State = produce(Global.State, (s) => {
// s[hash] = state
// })
const selector = () => {
Global.mutableState[storeId].count = 0
Global.currentStoreId = storeId
Expand Down
53 changes: 27 additions & 26 deletions src/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,33 @@ const getNewState: Middleware = async (context, restMiddlewares) => {
return await next(restMiddlewares)
}

const getNewStateWithCache =
(maxTime: number = 5000): Middleware =>
async (context, restMiddlewares) => {
const {
action,
Global,
modelName,
consumerActions,
params,
next,
actionName
} = context
context.newState =
(await Promise.race([
action(params, {
actions: consumerActions(Global.Actions[modelName], { modelName }),
state: Global.State[modelName]
}),
timeout(maxTime, getCache(modelName, actionName))
])) || null
return await next(restMiddlewares)
}
const getNewStateWithCache = (maxTime: number = 5000): Middleware => async (
context,
restMiddlewares
) => {
const {
action,
Global,
modelName,
consumerActions,
params,
next,
actionName
} = context
context.newState =
(await Promise.race([
action(params, {
actions: consumerActions(Global.Actions[modelName], { modelName }),
state: Global.State[modelName]
}),
timeout(maxTime, getCache(modelName, actionName))
])) || null
return await next(restMiddlewares)
}

const setNewState: Middleware = async (context, restMiddlewares) => {
const { modelName, newState, next, Global } = context
if (Global.Setter.functionSetter[modelName]) {
const { modelName, newState, next, Global, disableSelectorUpdate } = context
if (Global.Setter.functionSetter[modelName] && !disableSelectorUpdate) {
Object.keys(Global.Setter.functionSetter[modelName]).map((key) => {
const setter = Global.Setter.functionSetter[modelName][key]
if (setter) {
Expand Down Expand Up @@ -153,15 +154,15 @@ const devToolsListener: Middleware = async (context, restMiddlewares) => {
}

const communicator: Middleware = async (context, restMiddlewares) => {
const { modelName, next, Global } = context
const { modelName, next, Global, disableSelectorUpdate } = context
if (Global.Setter.classSetter) {
Global.Setter.classSetter(Global.State)
}
if (Global.Setter.functionSetter[modelName]) {
Object.keys(Global.Setter.functionSetter[modelName]).map((key) => {
const setter = Global.Setter.functionSetter[modelName][key]
if (setter) {
if (!setter.selector) {
if (!setter.selector || disableSelectorUpdate) {
setter.setState(Global.State[modelName])
} else {
const newSelectorRef = setter.selector(Global.State[modelName])
Expand Down

0 comments on commit 8f7f137

Please sign in to comment.