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]) {