diff --git a/README.md b/README.md
index 183427b..2581f58 100644
--- a/README.md
+++ b/README.md
@@ -17,11 +17,11 @@ The State management library for React
🐛 Debug easily on test environment
```tsx
-import { useModel, createStore } from 'react-model'
+import { model, createStore } from 'react-model'
// define model
const useTodo = () => {
- const [items, setItems] = useModel(['Install react-model', 'Read github docs', 'Build App'])
+ const [items, setItems] = model(['Install react-model', 'Read github docs', 'Build App'])
return { items, setItems }
}
@@ -50,7 +50,7 @@ const TodoList = () => {
## Quick Start
-[createStore + useModel](https://codesandbox.io/s/createstore-usemodal-all-of-your-state-4u8s6)
+[createStore + model](https://codesandbox.io/s/createstore-usemodal-all-of-your-state-4u8s6)
[CodeSandbox: TodoMVC](https://codesandbox.io/s/moyxon99jx)
@@ -79,10 +79,12 @@ npm install react-model
- [SSR with Next.js](#ssr-with-nextjs)
- [Middleware](#middleware)
- [Expand Context](#expand-context)
+ - [Concurrent Mode Support](#concurrent-mode-support)
- [Other Concept required by Class Component](#other-concept-required-by-class-component)
- [Provider](#provider)
- [connect](#connect)
- [FAQ](#faq)
+ - [Migrate from 4.1.x to 5.x.x](#migrate-from-41x-to-5xx)
- [Migrate from 4.0.x to 4.1.x](#migrate-from-40x-to-41x)
- [Migrate from 3.1.x to 4.x.x](#migrate-from-31x-to-4xx)
- [How can I disable the console debugger?](#how-can-i-disable-the-console-debugger)
@@ -105,10 +107,10 @@ You can create a shared / local store by createStore api.
```typescript
import { useState } from 'react'
-import { useModel } from 'react-model'
+import { model } from 'react-model'
const { useStore } = createStore(() => {
const [localCount, setLocalCount] = useState(1) // Local State, Independent in different components
- const [count, setCount] = useModel(1) // Global State, the value is the same in different components
+ const [count, setCount] = model(1) // Global State, the value is the same in different components
const incLocal = () => {
setLocalCount(localCount + 1)
}
@@ -587,6 +589,46 @@ actions.ext()
[⇧ back to top](#table-of-contents)
+### Concurrent Mode Support
+
+To achieve [level-3 support](https://github.com/reactwg/react-18/discussions/70#discussion-3450022) for concurrent mode for specific state, you should only depend on React state.
+
+react-model provide `useModel` for developers out-of-box.
+
+It only needs a few changes for the startup example.
+
+```tsx
+import { Provider, useModel, createStore } from 'react-model'
+
+// define model
+const useTodo = () => {
+ // changes 1: model -> useModel
+ const [items, setItems] = useModel(['Install react-model', 'Read github docs', 'Build App'])
+ return { items, setItems }
+}
+
+// Model Register
+// NOTE : getState is only valid inside hooks component if Todo contains custom hooks like useModel / useState
+const { useStore } = createStore(Todo)
+
+const App = () => {
+ // changes 2: wrap root components with Provider
+ return (
+
+
+
+ )
+}
+
+const TodoList = () => {
+ const { items, setItems } = useStore()
+ return
+
+ {state.items.map((item, index) => (
))}
+
+}
+```
+
## Other Concept required by Class Component
### Provider
@@ -695,6 +737,33 @@ export default connect(
## FAQ
+### Migrate from 4.1.x to 5.x.x
+
+1. replace useModel with model
+
+```tsx
+import { model } from 'react-model'
+const useTodo = () => {
+ // useModel -> model
+ const [items, setItems] = model(['Install react-model', 'Read github docs', 'Build App'])
+ return { items, setItems }
+}
+```
+
+2. wrap root component with Provider (required for concurrent mode)
+
+```tsx
+import { Provider } from 'react-model'
+const App = () => {
+ // changes 2: wrap root components with Provider
+ return (
+
+
+
+ )
+}
+```
+
### Migrate from 4.0.x to 4.1.x
1. replace Model with createStore
@@ -729,7 +798,7 @@ const Counter: ModelType<
// v4.1.x
const Counter = createStore(() => {
- const [state, setState] = useModel({ count: 0 })
+ const [state, setState] = model({ count: 0 })
const actions = {
increment: (params) => {
setState((state) => {
diff --git a/__test__/atom/lane.spec.tsx b/__test__/atom/lane.spec.tsx
new file mode 100644
index 0000000..d7d33b4
--- /dev/null
+++ b/__test__/atom/lane.spec.tsx
@@ -0,0 +1,515 @@
+///
+import * as React from 'react'
+import { renderHook, act } from '@testing-library/react-hooks'
+import { render } from '@testing-library/react'
+import { act as RAct } from 'react-dom/test-utils'
+import { createStore, Model, useModel, Provider } from '../../src'
+
+describe('lane model', () => {
+ test('single model', () => {
+ const wrapper = Provider
+ const { useStore } = createStore(() => {
+ const [count, setCount] = useModel(1)
+ return { count, setCount }
+ })
+ let renderTimes = 0
+ const { result } = renderHook(
+ () => {
+ const { count, setCount } = useStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount }
+ },
+ { wrapper }
+ )
+
+ act(() => {
+ expect(result.current.renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ })
+
+ act(() => {
+ result.current.setCount(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(5)
+ })
+ })
+
+ test('create store with namespace', () => {
+ const wrapper = Provider
+ const { useStore } = Model({})
+ createStore('Shared', () => {
+ const [count, setCount] = useModel(1)
+ return { count, setCount }
+ })
+
+ let renderTimes = 0
+ const { result } = renderHook(
+ () => {
+ // @ts-ignore
+ const { count, setCount } = useStore('Shared')
+ renderTimes += 1
+ return { renderTimes, count, setCount }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ })
+
+ act(() => {
+ // @ts-ignore
+ result.current.setCount(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(5)
+ })
+ })
+
+ test('subscribe model', () => {
+ const wrapper = Provider
+ let subscribeTimes = 0
+ const { subscribe, unsubscribe, getState } = createStore(() => {
+ const [count, setCount] = useModel(1)
+ return { count, setCount }
+ })
+
+ const { result } = renderHook(
+ () => {
+ const state = getState()
+ return { state }
+ },
+ { wrapper }
+ )
+
+ const callback_1 = () => {
+ subscribeTimes += 1
+ }
+
+ const callback_2 = () => {
+ subscribeTimes += 1
+ }
+
+ subscribe(callback_1)
+ subscribe(callback_2)
+
+ act(() => {
+ expect(subscribeTimes).toEqual(0)
+ })
+
+ act(() => {
+ result.current.state.setCount(5)
+ })
+
+ act(() => {
+ expect(subscribeTimes).toEqual(2)
+ expect(result.current.state.count).toBe(5)
+ })
+
+ unsubscribe(callback_1)
+
+ act(() => {
+ result.current.state.setCount(15)
+ })
+
+ act(() => {
+ expect(subscribeTimes).toEqual(3)
+ expect(result.current.state.count).toBe(15)
+ })
+ })
+
+ test('pass function to useModel ', () => {
+ const wrapper = Provider
+ const { useStore } = createStore(() => {
+ const [count, setCount] = useModel(() => 1)
+ return { count, setCount }
+ })
+ let renderTimes = 0
+ const { result } = renderHook(
+ () => {
+ const { count, setCount } = useStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount }
+ },
+ { wrapper }
+ )
+
+ act(() => {
+ expect(renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ })
+
+ act(() => {
+ result.current.setCount((count) => count + 1)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(2)
+ })
+ })
+
+ test('false value can be accepted', () => {
+ const wrapper = Provider
+ const { useStore } = createStore(() => {
+ const [count, setCount] = useModel(true)
+ return { count, setCount }
+ })
+
+ let renderTimes = 0
+ const { result, rerender } = renderHook(
+ () => {
+ const { count, setCount } = useStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(renderTimes).toEqual(1)
+ expect(result.current.count).toBe(true)
+ })
+
+ act(() => {
+ result.current.setCount(false)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(false)
+ })
+
+ act(() => {
+ rerender()
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(3)
+ expect(result.current.count).toBe(false)
+ })
+ })
+
+ test('array value is protected', () => {
+ const wrapper = Provider
+ const { useStore } = createStore(() => {
+ const [list, setList] = useModel([] as number[])
+ return { list, setList }
+ })
+
+ let renderTimes = 0
+ const { result, rerender } = renderHook(
+ () => {
+ const { list, setList } = useStore()
+ renderTimes += 1
+ return { renderTimes, list, setList }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(renderTimes).toEqual(1)
+ expect(result.current.list.constructor.name).toBe('Array')
+ })
+
+ act(() => {
+ result.current.setList([1, 2])
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.list.constructor.name).toBe('Array')
+ expect(result.current.list[0]).toBe(1)
+ expect(result.current.list[1]).toBe(2)
+ })
+
+ act(() => {
+ rerender()
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(3)
+ expect(result.current.list.constructor.name).toBe('Array')
+ expect(result.current.list[0]).toBe(1)
+ expect(result.current.list[1]).toBe(2)
+ })
+ })
+
+ test('object value is merged', () => {
+ const wrapper = Provider
+ const { useStore } = createStore(() => {
+ const [obj, setObj] = useModel({ name: 'Bob', age: 17 })
+ return { obj, setObj }
+ })
+
+ let renderTimes = 0
+ const { result, rerender } = renderHook(
+ () => {
+ const { obj, setObj } = useStore()
+ renderTimes += 1
+ return { renderTimes, obj, setObj }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(renderTimes).toEqual(1)
+ expect(result.current.obj.age).toBe(17)
+ expect(result.current.obj.name).toBe('Bob')
+ })
+
+ act(() => {
+ result.current.setObj({ age: 18 })
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.obj.name).toBe('Bob')
+ expect(result.current.obj.age).toBe(18)
+ })
+
+ act(() => {
+ rerender()
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(3)
+ expect(result.current.obj.name).toBe('Bob')
+ expect(result.current.obj.age).toBe(18)
+ })
+
+ act(() => {
+ result.current.setObj({ name: 'Bom' })
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(4)
+ expect(result.current.obj.name).toBe('Bom')
+ expect(result.current.obj.age).toBe(18)
+ })
+ })
+
+ test('multiple models', () => {
+ const wrapper = Provider
+ const { useStore } = createStore(() => {
+ const [count, setCount] = useModel(1)
+ const [name, setName] = useModel('Jane')
+ return { count, name, setName, setCount }
+ })
+ let renderTimes = 0
+ const { result } = renderHook(
+ () => {
+ const { count, setCount, name, setName } = useStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount, name, setName }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ })
+
+ act(() => {
+ result.current.setCount(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(5)
+ expect(result.current.name).toBe('Jane')
+ })
+
+ act(() => {
+ result.current.setName('Bob')
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(3)
+ expect(result.current.name).toBe('Bob')
+ expect(result.current.count).toBe(5)
+ })
+ })
+
+ test('multiple stores', () => {
+ const wrapper = Provider
+ const { useStore } = createStore(() => {
+ const [count, setCount] = useModel(1)
+
+ return { count, setCount }
+ })
+
+ const { useStore: useOtherStore } = createStore(() => {
+ const [name, setName] = useModel('Jane')
+ return { name, setName }
+ })
+ let renderTimes = 0
+ const { result } = renderHook(
+ () => {
+ const { count, setCount } = useStore()
+ const { name, setName } = useOtherStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount, name, setName }
+ },
+ { wrapper }
+ )
+
+ act(() => {
+ expect(renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ })
+
+ act(() => {
+ result.current.setCount(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(5)
+ expect(result.current.name).toBe('Jane')
+ })
+
+ act(() => {
+ result.current.setName('Bob')
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(3)
+ expect(result.current.name).toBe('Bob')
+ expect(result.current.count).toBe(5)
+ })
+ })
+
+ test('share single model between components', () => {
+ const wrapper = Provider
+ const { useStore } = createStore(() => {
+ const [count, setCount] = useModel(1)
+ return { count, setCount }
+ })
+ let renderTimes = 0
+ const { result } = renderHook(
+ () => {
+ const { count, setCount } = useStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount }
+ },
+ { wrapper }
+ )
+
+ const { result: mirrorResult } = renderHook(
+ () => {
+ const { count, setCount } = useStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(mirrorResult.current.count).toBe(1)
+ })
+
+ act(() => {
+ result.current.setCount(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(4)
+ expect(mirrorResult.current.count).toBe(5)
+ })
+ })
+
+ test('complex case', () => {
+ const results: any = {}
+ function TestComponent({ id, hook }: any) {
+ results[id] = hook()
+ return null
+ }
+
+ const { useStore } = createStore(() => {
+ const [count, setCount] = useModel(1)
+ const [name, setName] = useModel('Jane')
+ return { count, setCount, name, setName }
+ })
+ const { useStore: useOtherStore } = createStore(() => {
+ const [data, setData] = useModel({ status: 'UNKNOWN' })
+ return { data, setData }
+ })
+ const { useStore: useOnce } = createStore(() => {
+ const [status, set] = useModel(false)
+ return { status, set }
+ })
+ let renderTimes = 0
+
+ render(
+
+ {
+ const { count, setCount } = useStore()
+ const { setData } = useOtherStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount, setData }
+ }}
+ />
+ {
+ const { setName, name } = useStore()
+ const { data } = useOtherStore()
+ const { status, set } = useOnce()
+ renderTimes += 1
+ return { renderTimes, data, setName, name, status, set }
+ }}
+ />
+
+ )
+
+ RAct(() => {
+ expect(renderTimes).toEqual(2)
+ expect(results.second.name).toBe('Jane')
+ })
+
+ RAct(() => {
+ results.first.setData({ status: 'SUCCESS' })
+ })
+
+ RAct(() => {
+ expect(renderTimes).toEqual(4)
+ expect(results.second.data).toEqual({ status: 'SUCCESS' })
+ })
+
+ RAct(() => {
+ results.second.setName('Bob')
+ })
+
+ RAct(() => {
+ expect(renderTimes).toEqual(6)
+ expect(results.second.name).toBe('Bob')
+ expect(results.second.status).toBe(false)
+ })
+
+ RAct(() => {
+ results.second.set(true)
+ })
+
+ RAct(() => {
+ expect(renderTimes).toEqual(8)
+ expect(results.second.status).toBe(true)
+ })
+
+ RAct(() => {
+ results.second.setName('Jane')
+ })
+
+ RAct(() => {
+ expect(renderTimes).toEqual(10)
+ expect(results.second.name).toBe('Jane')
+ expect(results.second.status).toBe(true)
+ expect(results.second.name).toBe('Jane')
+ expect(results.first.count).toBe(1)
+ })
+ })
+})
diff --git a/__test__/atom/migrate.spec.ts b/__test__/atom/migrate.spec.ts
new file mode 100644
index 0000000..8f80fb1
--- /dev/null
+++ b/__test__/atom/migrate.spec.ts
@@ -0,0 +1,63 @@
+///
+import { renderHook, act } from '@testing-library/react-hooks'
+import { createStore, useModel, Provider } from '../../src'
+
+describe('migrate test', () => {
+ test('migrate from v4.0.x', () => {
+ const wrapper = Provider
+ const store = createStore(() => {
+ const [state, setState] = useModel({ count: 0, otherKey: 'key' })
+ const actions = {
+ add: (params: number) => {
+ return setState({
+ count: state.count + params
+ })
+ },
+ addCaller: () => {
+ actions.add(5)
+ },
+ increment: (params: number) => {
+ return setState((state) => {
+ state.count += params
+ })
+ }
+ }
+ return [state, actions] as const
+ })
+
+ let renderTimes = 0
+
+ const { result } = renderHook(
+ () => {
+ renderTimes += 1
+ const [state, actions] = store.useStore()
+ return { state, renderTimes, actions }
+ },
+ { wrapper }
+ )
+
+ act(() => {
+ expect(result.current.renderTimes).toEqual(1)
+ expect(result.current.state.count).toBe(0)
+ })
+
+ act(() => {
+ result.current.actions.addCaller()
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.state.count).toBe(5)
+ expect(result.current.state.otherKey).toBe('key')
+ })
+
+ act(() => {
+ result.current.actions.increment(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(3)
+ expect(result.current.state.count).toBe(10)
+ })
+ })
+})
diff --git a/__test__/atom/react.spec.tsx b/__test__/atom/react.spec.tsx
new file mode 100644
index 0000000..ab7b180
--- /dev/null
+++ b/__test__/atom/react.spec.tsx
@@ -0,0 +1,233 @@
+///
+import { renderHook, act } from '@testing-library/react-hooks'
+import { render } from '@testing-library/react'
+import { act as RAct } from 'react-dom/test-utils'
+import { createStore, useModel, Provider } from '../../src'
+import { useState, useEffect } from 'react'
+import * as React from 'react'
+
+describe('compatible with useState + useEffect', () => {
+ test('compatible with useState', () => {
+ const wrapper = Provider
+ 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 }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(result.current.renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ })
+
+ act(() => {
+ result.current.setCount(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(5)
+ })
+ })
+
+ test('useEffect', () => {
+ const wrapper = Provider
+ let renderTimes = 0
+ let createTimes = 0
+ let updateTimes = 0
+ // A
+ const { result } = renderHook(
+ () => {
+ const [count, setCount] = useState(1)
+ useEffect(() => {
+ createTimes += 1
+ }, [])
+ useEffect(() => {
+ updateTimes += 1
+ }, [count])
+
+ renderTimes += 1
+ return { renderTimes, count, setCount }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(result.current.renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ expect(createTimes).toBe(1)
+ expect(updateTimes).toBe(1)
+ })
+
+ act(() => {
+ result.current.setCount(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(5)
+ expect(createTimes).toBe(1)
+ expect(updateTimes).toBe(2)
+ })
+ })
+
+ test('compatible with useEffect', () => {
+ const wrapper = Provider
+ let renderTimes = 0
+ let createTimes = 0
+ let updateTimes = 0
+ // 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 }
+ },
+ { wrapper }
+ )
+ act(() => {
+ expect(result.current.renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ expect(createTimes).toBe(1)
+ expect(updateTimes).toBe(1)
+ })
+
+ act(() => {
+ result.current.setCount(5)
+ })
+
+ 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', () => {
+ const wrapper = Provider
+ 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 }
+ },
+ { wrapper }
+ )
+ act(async () => {
+ expect(result.current.renderTimes).toEqual(1)
+ expect(result.current.count).toBe(1)
+ })
+
+ act(async () => {
+ await result.current.setCount(5)
+ })
+
+ act(() => {
+ expect(renderTimes).toEqual(2)
+ expect(result.current.count).toBe(5)
+ })
+ })
+
+ test('combine useState and useStore', () => {
+ // https://github.com/testing-library/react-hooks-testing-library/issues/615#issuecomment-840512255
+ const results: any = {}
+ function TestComponent({ id, hook }: any) {
+ results[id] = hook()
+ return null
+ }
+
+ let renderTimes = 0
+
+ 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)
+
+ render(
+
+ {
+ const { count, setCount, name, setName } = useStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount, name, setName }
+ }}
+ />
+ {
+ const { count, setCount, name } = useStore()
+ renderTimes += 1
+ return { renderTimes, count, setCount, name }
+ }}
+ />
+
+ )
+
+ RAct(() => {
+ expect(results.first.renderTimes).toBe(1)
+ expect(results.second.renderTimes).toBe(2)
+ expect(results.first.count).toBe(1)
+ })
+
+ RAct(() => {
+ results.second.setCount(5)
+ })
+
+ RAct(() => {
+ expect(results.first.renderTimes).toEqual(1)
+ expect(results.second.renderTimes).toEqual(3)
+ expect(results.second.count).toBe(5)
+ expect(results.first.count).toBe(1)
+ })
+
+ RAct(() => {
+ results.first.setCount(50)
+ })
+
+ RAct(() => {
+ expect(results.first.renderTimes).toEqual(4)
+ expect(results.second.renderTimes).toEqual(3)
+ expect(results.second.count).toBe(5)
+ expect(results.first.count).toBe(50)
+ })
+
+ RAct(() => {
+ results.first.setName('Bob')
+ })
+
+ RAct(() => {
+ expect(results.first.renderTimes).toEqual(5)
+ expect(results.second.renderTimes).toEqual(6)
+ expect(results.first.name).toBe('Bob')
+ expect(results.second.name).toBe('Bob')
+ })
+ })
+})
diff --git a/__test__/lane/lane.spec.ts b/__test__/lane/lane.spec.ts
index 67d2d61..eec9d83 100644
--- a/__test__/lane/lane.spec.ts
+++ b/__test__/lane/lane.spec.ts
@@ -1,11 +1,11 @@
///
import { renderHook, act } from '@testing-library/react-hooks'
-import { createStore, useModel, Model } from '../../src'
+import { createStore, model, Model } from '../../src'
describe('lane model', () => {
test('single model', async () => {
const { useStore } = createStore(() => {
- const [count, setCount] = useModel(1)
+ const [count, setCount] = model(1)
return { count, setCount }
})
let renderTimes = 0
@@ -33,7 +33,7 @@ describe('lane model', () => {
test('create store with namespace', async () => {
const { useStore } = Model({})
createStore('Shared', () => {
- const [count, setCount] = useModel(1)
+ const [count, setCount] = model(1)
return { count, setCount }
})
@@ -65,7 +65,7 @@ describe('lane model', () => {
test('subscribe model', () => {
let subscribeTimes = 0
const { subscribe, unsubscribe, getState } = createStore(() => {
- const [count, setCount] = useModel(1)
+ const [count, setCount] = model(1)
return { count, setCount }
})
@@ -105,9 +105,9 @@ describe('lane model', () => {
})
})
- test('pass function to useModel ', async () => {
+ test('pass function to model ', async () => {
const { useStore } = createStore(() => {
- const [count, setCount] = useModel(() => 1)
+ const [count, setCount] = model(() => 1)
return { count, setCount }
})
let renderTimes = 0
@@ -134,7 +134,7 @@ describe('lane model', () => {
test('false value can be accepted', async () => {
const { useStore } = createStore(() => {
- const [count, setCount] = useModel(true)
+ const [count, setCount] = model(true)
return { count, setCount }
})
@@ -170,7 +170,7 @@ describe('lane model', () => {
test('array value is protected', async () => {
const { useStore } = createStore(() => {
- const [list, setList] = useModel(>[])
+ const [list, setList] = model(>[])
return { list, setList }
})
@@ -210,8 +210,8 @@ describe('lane model', () => {
test('multiple models', async () => {
const { useStore } = createStore(() => {
- const [count, setCount] = useModel(1)
- const [name, setName] = useModel('Jane')
+ const [count, setCount] = model(1)
+ const [name, setName] = model('Jane')
return { count, name, setName, setCount }
})
let renderTimes = 0
@@ -248,13 +248,13 @@ describe('lane model', () => {
test('multiple stores', async () => {
const { useStore } = createStore(() => {
- const [count, setCount] = useModel(1)
+ const [count, setCount] = model(1)
return { count, setCount }
})
const { useStore: useOtherStore } = createStore(() => {
- const [name, setName] = useModel('Jane')
+ const [name, setName] = model('Jane')
return { name, setName }
})
let renderTimes = 0
@@ -293,7 +293,7 @@ describe('lane model', () => {
test('share single model between components', async () => {
const { useStore } = createStore(() => {
- const [count, setCount] = useModel(1)
+ const [count, setCount] = model(1)
return { count, setCount }
})
let renderTimes = 0
@@ -325,16 +325,16 @@ describe('lane model', () => {
test('complex case', async () => {
const { useStore, getState } = createStore(() => {
- const [count, setCount] = useModel(1)
- const [name, setName] = useModel('Jane')
+ const [count, setCount] = model(1)
+ const [name, setName] = model('Jane')
return { count, setCount, name, setName }
})
const { useStore: useOtherStore } = createStore(() => {
- const [data, setData] = useModel({ status: 'UNKNOWN' })
+ const [data, setData] = model({ status: 'UNKNOWN' })
return { data, setData }
})
const { useStore: useOnce } = createStore(() => {
- const [status, set] = useModel(false)
+ const [status, set] = model(false)
return { status, set }
})
let renderTimes = 0
diff --git a/__test__/lane/migrate.spec.ts b/__test__/lane/migrate.spec.ts
index 59a5bf2..2e3149e 100644
--- a/__test__/lane/migrate.spec.ts
+++ b/__test__/lane/migrate.spec.ts
@@ -1,11 +1,11 @@
///
import { renderHook, act } from '@testing-library/react-hooks'
-import { createStore, useModel } from '../../src'
+import { createStore, model } from '../../src'
describe('migrate test', async () => {
test('migrate from v4.0.x', async () => {
const store = createStore(() => {
- const [state, setState] = useModel({ count: 0, otherKey: 'key' })
+ const [state, setState] = model({ count: 0, otherKey: 'key' })
const actions = {
add: (params: number) => {
return setState({
@@ -51,7 +51,6 @@ describe('migrate test', async () => {
result.current.actions.increment(5)
})
-
act(() => {
expect(renderTimes).toEqual(3)
expect(result.current.state.count).toBe(10)
diff --git a/__test__/lane/react.spec.ts b/__test__/lane/react.spec.ts
index f53dba4..51652db 100644
--- a/__test__/lane/react.spec.ts
+++ b/__test__/lane/react.spec.ts
@@ -1,6 +1,6 @@
///
import { renderHook, act } from '@testing-library/react-hooks'
-import { createStore, useModel } from '../../src'
+import { createStore, model } from '../../src'
import { useState, useEffect } from 'react'
describe('compatible with useState + useEffect', () => {
@@ -136,8 +136,8 @@ describe('compatible with useState + useEffect', () => {
const useCount = () => {
// useState create local state
const [count, setCount] = useState(1)
- // useModel create shared state
- const [name, setName] = useModel('Jane')
+ // model create shared state
+ const [name, setName] = model('Jane')
return { count, setCount, name, setName }
}
const { useStore } = createStore(useCount)
diff --git a/package.json b/package.json
index e240cc2..28ea2a3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-model",
- "version": "4.2.0-rc.2",
+ "version": "5.0.0-alpha.0",
"description": "The State management library for React",
"main": "./dist/react-model.js",
"module": "./dist/react-model.esm.js",
diff --git a/src/global.ts b/src/global.ts
index 240ff8d..e21cb73 100644
--- a/src/global.ts
+++ b/src/global.ts
@@ -24,7 +24,7 @@ let withDevTools = false
let uid = 0 // The unique id of hooks
let storeId = 0 // The unique id of stores
-let currentStoreId = '0' // Used for useModel
+let currentStoreId = '0' // Used for model
let gid = 0 // The unique id of models' group
export default {
diff --git a/src/index.d.ts b/src/index.d.ts
index 139d697..776f429 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -97,7 +97,7 @@ interface BaseContext {
interface InnerContext extends BaseContext {
// Actions with function type context will always invoke current component's reload.
- // f -> function, o -> outer, c -> class, u -> useModel
+ // f -> function, o -> outer, c -> class, u -> model
type?: 'f' | 'o' | 'c' | 'u'
__hash?: string
}
diff --git a/src/index.tsx b/src/index.tsx
index ce9e28d..b40efdc 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -8,6 +8,7 @@ import {
useLayoutEffect,
useEffect,
useState,
+ useContext,
useRef
} from 'react'
import Global from './global'
@@ -31,9 +32,9 @@ const isAPI = (input: any): input is API => {
return (input as API).useStore !== undefined
}
-// useModel rules:
-// DON'T USE useModel OUTSIDE createStore func
-function useModel(
+// model rules:
+// DON'T USE model OUTSIDE createStore func
+function model(
state: S | (() => S)
): [S, (state: Partial | ((state: S) => S | void)) => void] {
const storeId = Global.currentStoreId
@@ -95,6 +96,98 @@ function useModel(
return [Global.mutableState[storeId][index], setter]
}
+function useModel(
+ state: S | (() => S)
+): [S, (state: Partial | ((state: S) => S | void)) => void] {
+ const storeId = Global.currentStoreId
+ const index = Global.mutableState[storeId].count
+ Global.mutableState[storeId].count += 1
+ const globalStates = useContext(GlobalContext)
+ if (!Global.mutableState[storeId].hasOwnProperty(index)) {
+ if (typeof state === 'function') {
+ // @ts-ignore
+ Global.mutableState[storeId][index] = state()
+ } else {
+ Global.mutableState[storeId][index] = state
+ }
+ }
+
+ const setter = async (state: Partial | ((prevState: S) => S | void)) => {
+ const context: InnerContext = {
+ Global,
+ action: () => {
+ if (!Global.Setter.classSetter) return
+ if (typeof state === 'function') {
+ Global.Setter.classSetter((stores: any) => {
+ let newState: any
+ if (!stores[storeId].hasOwnProperty(index)) {
+ newState = produce(
+ Global.mutableState[storeId][index],
+ // @ts-ignore
+ state
+ )
+ } else {
+ newState = produce(
+ stores[storeId][index],
+ // @ts-ignore
+ state
+ )
+ }
+ return produce(stores, (s: any) => {
+ s[storeId][index] = newState
+ })
+ })
+ } else {
+ Global.Setter.classSetter((stores: any) => {
+ if (
+ stores[storeId][index] &&
+ state &&
+ stores[storeId][index].constructor.name === 'Object' &&
+ state.constructor.name === 'Object'
+ ) {
+ return produce(stores, (s: any) => {
+ s[storeId][index] = { ...s[storeId][index], ...state }
+ })
+ } else if (
+ state.constructor.name === 'Object' &&
+ !stores[storeId][index] &&
+ Global.mutableState[storeId][index].constructor.name === 'Object'
+ ) {
+ return produce(stores, (s: any) => {
+ s[storeId][index] = {
+ ...Global.mutableState[storeId][index],
+ ...state
+ }
+ })
+ } else {
+ return produce(stores, (s: any) => {
+ s[storeId][index] = state
+ })
+ }
+ })
+ }
+ },
+ actionName: 'setter',
+ consumerActions,
+ disableSelectorUpdate: true,
+ middlewareConfig: {},
+ modelName: storeId,
+ newState: {},
+ params: undefined,
+ type: 'u'
+ }
+
+ // pass update event to other components subscribe the same store
+ return await applyMiddlewares(actionMiddlewares, context)
+ }
+ return [
+ globalStates[storeId][index] === undefined
+ ? Global.mutableState[storeId][index]
+ : globalStates[storeId][index],
+ setter
+ ]
+}
+
function createStore(useHook: CustomModelHook): LaneAPI
function createStore(name: string, useHook: CustomModelHook): LaneAPI
function createStore(n: any, u?: any): LaneAPI {
@@ -107,6 +200,11 @@ function createStore(n: any, u?: any): LaneAPI {
if (!Global.mutableState[storeId]) {
Global.mutableState[storeId] = { count: 0 }
}
+ if (!Global.State[storeId]) {
+ Global.State = produce(Global.State, (s) => {
+ s[storeId] = {}
+ })
+ }
// Global.currentStoreId = storeId
// const state = useHook()
// Global.State = produce(Global.State, (s) => {
@@ -448,6 +546,7 @@ export {
actionMiddlewares,
createStore,
useModel,
+ model,
Model,
middlewares,
Provider,
diff --git a/src/middlewares.ts b/src/middlewares.ts
index 7ce73f7..3af64dc 100644
--- a/src/middlewares.ts
+++ b/src/middlewares.ts
@@ -178,8 +178,8 @@ const devToolsListener: Middleware = async (context, restMiddlewares) => {
}
const communicator: Middleware = async (context, restMiddlewares) => {
- const { modelName, next, Global, disableSelectorUpdate } = context
- if (Global.Setter.classSetter) {
+ const { modelName, next, Global, disableSelectorUpdate, type } = context
+ if (Global.Setter.classSetter && type !== 'u') {
Global.Setter.classSetter(Global.State)
}
if (Global.Setter.functionSetter[modelName]) {