├── .codecov.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── custom.md
│ └── issue-template.md
└── workflows
│ └── node.js.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── .remarkrc
├── .travis.yml
├── README.md
├── __test__
├── Model
│ ├── error.multi.spec.ts
│ ├── error.spec.ts
│ ├── errorModel.ts
│ ├── extContext.multi.spec.ts
│ ├── extContext.spec.single.ts
│ ├── mixed.spec.ts
│ ├── return.spec.ts
│ ├── same-name.spec.ts
│ ├── share-setState.spec.ts
│ └── single.spec.ts
├── SSR
│ └── index.spec.ts
├── actions
│ ├── actions.spec.ts
│ ├── getActions.spec.ts
│ ├── unmount.spec.ts
│ └── useStore.spec.ts
├── asyncState.spec.ts
├── class
│ ├── class.spec.tsx
│ ├── communicator.spec.tsx
│ ├── mapActions.spec.tsx
│ └── renderProps.spec.tsx
├── connect.spec.tsx
├── disable-dubugger.spec.ts
├── dubugger.spec.ts
├── getActions.spec.ts
├── getState.spec.ts
├── index.d.ts
├── index.ts
├── lane
│ ├── lane.spec.ts
│ ├── migrate.spec.ts
│ └── react.spec.ts
├── middlewares
│ ├── commuicator.spec.ts
│ ├── devToolsListener.spec.ts
│ ├── getNewStateWithCache.spec.ts
│ ├── middlewareConfig.spec.ts
│ ├── model.spec.ts
│ ├── subscribe.spec.ts
│ ├── tryCatch.spec.ts
│ └── unsubscribe.spec.ts
├── performance
│ └── deep-expensive-mutation.spec.tsx
├── selector
│ ├── model.spec.ts
│ ├── models.spec.ts
│ └── shallowEqual.spec.ts
└── useStore
│ ├── actions.spec.ts
│ ├── initial.spec.ts
│ └── initialModels.spec.ts
├── commitlint.config.js
├── jest.config.js
├── package.json
├── src
├── global.ts
├── helper.ts
├── index.d.ts
├── index.tsx
└── middlewares.ts
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | branch: master
3 |
4 | coverage:
5 | status:
6 | project:
7 | default:
8 | # Fail the status if coverage drops by >= 0.1%
9 | threshold: 0.1
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/custom.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Issue Template (Chinese)'
3 | about: '规范的issue可以帮助我们更好地定位问题'
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | 我有一个:
11 |
12 | 1. [ ] 问题: 我们很乐意解决你遇到的问题.😄
13 |
14 | 2. [ ] 文档补充/修正. 欢迎提PR修正👏.
15 |
16 | 2. [ ] Issue:
17 |
18 | * [ ] **请提供完整的报错信息** ❌
19 |
20 | * [ ] 请提供issue的**最小**复现单元. **你可以在这个[sandbox](https://codesandbox.io/s/moyxon99jx)基础上补充复现问题的代码** 🚀
21 |
22 | * [ ] 你是否确定这个问题没有在issue区被提过? 🤔
23 |
24 | * [ ] 请详细描述你的issue. 你期望的行为是什么 ❓
25 |
26 | * [ ] 请提供你使用的react-model版本和issue复现所需的环境. (浏览器 / node / 操作系统 / react 版本? 🚧
27 |
28 | | 依赖环境 | 版本(s) |
29 | | ---------------- | ---------- |
30 | | react-model | |
31 | | Node | |
32 | | react | |
33 | | Operating System | |
34 |
35 | 3. [ ] 好主意、想加入的新feature:
36 | * [ ] 它可以解决什么问题? 🐛
37 | * [ ] 你认为它应该被集成在这个库中,还是以文档的方式说明,还是其他方式体现? 💡
38 | * [ ] 你是否乐意自己提PR来完成这个feature呢? ⚔
39 |
40 | 勾选你的问题所属的类型,issue模板中其他无关的部分记得删除哦
41 |
42 | **issue被解决后,希望你可以及时关闭issue :)**
43 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue Template
3 | about: Good issue can help others to position problem easily.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | I have a:
11 |
12 | 1. [ ] Question: Feel free to just state your question.😄
13 |
14 | 2. [ ] Documentation improvement. Creating a PR if you can👏.
15 |
16 | 2. [ ] Issue:
17 |
18 | * [ ] **Provide error messages including stacktrace** ❌
19 |
20 | * [ ] Provide a **minimal** sample reproduction. **Create a reproduction based on this [sandbox](https://codesandbox.io/s/moyxon99jx)** 🚀
21 | * [ ] Did you check this issue wasn't filed before? 🤔
22 |
23 | * [ ] Elaborate on your issue. What behavior did you expect? ❓
24 |
25 | * [ ] State the versions of react-model and relevant libraries. Which browser / node / ... version? 🚧
26 |
27 | | Software | Version(s) |
28 | | ---------------- | ---------- |
29 | | react-model | |
30 | | Node | |
31 | | react | |
32 | | Operating System | |
33 |
34 | 3. [ ] Idea:
35 |
36 | * [ ] What problem would it solve for you? 🐛
37 | * [ ] Do you think others will benefit from this change as well and it should in core package? 💡
38 | * [ ] Are you willing to (attempt) a PR yourself? ⚔
39 |
40 | Please tick the appropriate boxes. Feel free to remove the _other_ sections.
41 |
42 | **Please be sure to close your issues promptly.**
43 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ main, dev ]
9 | pull_request:
10 | branches: [ main, dev ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: self-hosted
16 |
17 | strategy:
18 | matrix:
19 | node-version: [12.x, 14.x, 16.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v2
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v2
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'yarn'
29 | - run: yarn
30 | - run: yarn global add codecov
31 | - run: yarn test:coverage
32 | - run: codecov -f coverage/*.json
33 | - run: bash <(curl -s https://codecov.io/bash)
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Next.js
2 | .next/
3 | node_modules/
4 |
5 | # Parcel
6 | .cache/
7 |
8 | # package
9 | *.tgz
10 |
11 | # VSCode
12 | .vscode/
13 |
14 | # Compile
15 | dist/
16 |
17 | # microbundle
18 | .rts2_cache_cjs/
19 | .rts2_cache_es/
20 | .rts2_cache_umd/
21 |
22 | # yarn
23 | package-lock.json
24 |
25 | # Jest
26 | coverage/
27 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Next.js
2 | .next/
3 | node_modules/
4 |
5 | # Parcel
6 | .cache/
7 |
8 | # Example
9 | example/
10 |
11 | # package
12 | *.tgz
13 |
14 | # yarn
15 | yarn.lock
16 | package-lock.json
17 |
18 | # VSCode
19 | .vscode/
20 |
21 | # Test
22 | __test__/
23 | jest.config.js
24 | coverage/
25 |
26 | # microbundle
27 | .rts2_cache_cjs/
28 | .rts2_cache_es/
29 | .rts2_cache_umd/
30 |
31 | # Github
32 | .github/
33 | .git/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": false,
6 | "parser": "babylon",
7 | "singleQuote": true,
8 | "trailingComma": "none",
9 | "bracketSpacing": true,
10 | "jsxBracketSameLine": false,
11 | "overrides": [
12 | {
13 | "files": ".prettierrc",
14 | "options": { "parser": "json", "trailingComma": "none" }
15 | },
16 | {
17 | "files": ".babelrc",
18 | "options": { "parser": "json", "trailingComma": "none" }
19 | },
20 | {
21 | "files": "*.ts",
22 | "options": { "parser": "typescript", "trailingComma": "none" }
23 | },
24 | {
25 | "files": "*.tsx",
26 | "options": { "parser": "typescript", "trailingComma": "none" }
27 | },
28 | {
29 | "files": "*.json",
30 | "options": { "parser": "json", "trailingComma": "none" }
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/.remarkrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "remark-preset-lint-recommended",
4 | ["remark-lint-list-item-indent", "space"]
5 | ]
6 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '10'
4 | - '11'
5 | - '12'
6 | - '13'
7 | - '14'
8 |
9 | before_script:
10 | - npm install react@latest react-dom@latest
11 | - npm install codecov -g
12 |
13 | after_success:
14 | - npm run test:coverage
15 | - codecov -f coverage/*.json
16 | - bash <(curl -s https://codecov.io/bash)
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-model ·  [](https://www.npmjs.com/package/react-model) [](https://bundlephobia.com/result?p=react-model) [](https://github.com/byte-fe/react-model/actions/workflows/node.js.yml) [](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/react-model/dist/react-model.js) [](https://www.npmjs.com/package/react-model) [](https://codecov.io/gh/byte-fe/react-model) [](https://greenkeeper.io/) 
2 |
3 | The State management library for React
4 |
5 | 🎉 Support Both Class and Hooks Api
6 |
7 | ⚛️ Support [preact](https://github.com/byte-fe/react-model-experiment/tree/preact), react-native and Next.js
8 |
9 | ⚔ Full TypeScript Support
10 |
11 | 📦 Built with microbundle
12 |
13 | ⚙️ Middleware Pipline ( redux-devtools support ... )
14 |
15 | ☂️ 100% test coverage, safe on production
16 |
17 | 🐛 Debug easily on test environment
18 |
19 | ```tsx
20 | import { useModel, createStore } from 'react-model'
21 |
22 | // define model
23 | const useTodo = () => {
24 | const [items, setItems] = useModel(['Install react-model', 'Read github docs', 'Build App'])
25 | return { items, setItems }
26 | }
27 |
28 | // Model Register
29 | const { useStore } = createStore(useTodo)
30 |
31 | const App = () => {
32 | return
33 | }
34 |
35 | const TodoList = () => {
36 | const { items, setItems } = useStore()
37 | return
38 |
39 | {state.items.map((item, index) => (
))}
40 |
41 | }
42 | ```
43 |
44 | ---
45 |
46 | ## Recently Updated
47 |
48 | * [feat(middleware): support enable/disable sepecific middleware](#how-can-i-disable-the-console-debugger)
49 | * fix(stateupdater): fix the issue that setState on unmounted component
50 |
51 | ## Quick Start
52 |
53 | [createStore + useModel](https://codesandbox.io/s/createstore-usemodal-all-of-your-state-4u8s6)
54 |
55 | [CodeSandbox: TodoMVC](https://codesandbox.io/s/moyxon99jx)
56 |
57 | [Next.js + react-model work around](https://github.com/byte-fe/react-model-experiment)
58 |
59 | [v2 docs](https://github.com/byte-fe/react-model/blob/v2/README.md)
60 |
61 | install package
62 |
63 | ```shell
64 | npm install react-model
65 | ```
66 |
67 | ## Table of Contents
68 |
69 | - [Core Concept](#core-concept)
70 | - [createStore](#createstore)
71 | - [Model](#model)
72 | - [Model Register](#model-register)
73 | - [useStore](#usestore)
74 | - [getState](#getstate)
75 | - [actions](#actions)
76 | - [subscribe](#subscribe)
77 | - [Advance Concept](#advance-concept)
78 | - [immutable Actions](#immutable-actions)
79 | - [SSR with Next.js](#ssr-with-nextjs)
80 | - [Middleware](#middleware)
81 | - [Expand Context](#expand-context)
82 | - [Other Concept required by Class Component](#other-concept-required-by-class-component)
83 | - [Provider](#provider)
84 | - [connect](#connect)
85 | - [FAQ](#faq)
86 | - [Migrate from 4.0.x to 4.1.x](#migrate-from-40x-to-41x)
87 | - [Migrate from 3.1.x to 4.x.x](#migrate-from-31x-to-4xx)
88 | - [How can I disable the console debugger?](#how-can-i-disable-the-console-debugger)
89 | - [How can I add custom middleware](#how-can-i-add-custom-middleware)
90 | - [How can I make persist models](#how-can-i-make-persist-models)
91 | - [How can I deal with local state](#how-can-i-deal-with-local-state)
92 | - [How can I deal with huge dataset / circular dataset](#how-can-i-deal-with-huge-dataset--circular-dataset)
93 | - [actions throw error from immer.module.js](#actions-throw-error-from-immermodulejs)
94 | - [How can I customize each model's middlewares?](#how-can-i-customize-each-models-middlewares)
95 |
96 | ## Core Concept
97 |
98 | ### createStore
99 |
100 | You can create a shared / local store by createStore api.
101 |
102 | [Online Demo](https://codesandbox.io/s/createstore-usemodal-all-of-your-state-4u8s6)
103 |
104 | `model/counter.ts`
105 |
106 | ```typescript
107 | import { useState } from 'react'
108 | import { useModel } from 'react-model'
109 | const { useStore } = createStore(() => {
110 | const [localCount, setLocalCount] = useState(1) // Local State, Independent in different components
111 | const [count, setCount] = useModel(1) // Global State, the value is the same in different components
112 | const incLocal = () => {
113 | setLocalCount(localCount + 1)
114 | }
115 | const inc = () => {
116 | setCount(c => c + 1)
117 | }
118 | return { count, localCount, incLocal, inc }
119 | })
120 |
121 | export default useStore
122 | ```
123 |
124 | `page/counter-1.tsx`
125 |
126 | ```tsx
127 | import useSharedCounter from 'models/global-counter'
128 | const Page = () => {
129 | const { count, localCount, inc, incLocal } = useStore()
130 | return
131 | count: { count }
132 | localCount: { localCount }
133 |
134 |
135 |
136 | }
137 | ```
138 |
139 | ### Model
140 |
141 | Every model has its own state and actions.
142 |
143 | ```typescript
144 | const initialState = {
145 | counter: 0,
146 | light: false,
147 | response: {}
148 | }
149 |
150 | interface StateType {
151 | counter: number
152 | light: boolean
153 | response: {
154 | code?: number
155 | message?: string
156 | }
157 | }
158 |
159 | interface ActionsParamType {
160 | increment: number
161 | openLight: undefined
162 | get: undefined
163 | } // You only need to tag the type of params here !
164 |
165 | const model: ModelType = {
166 | actions: {
167 | increment: async (payload, { state }) => {
168 | return {
169 | counter: state.counter + (payload || 1)
170 | }
171 | },
172 | openLight: async (_, { state, actions }) => {
173 | await actions.increment(1) // You can use other actions within the model
174 | await actions.get() // support async functions (block actions)
175 | actions.get()
176 | await actions.increment(1) // + 1
177 | await actions.increment(1) // + 2
178 | await actions.increment(1) // + 3 as expected !
179 | return { light: !state.light }
180 | },
181 | get: async () => {
182 | await new Promise((resolve, reject) =>
183 | setTimeout(() => {
184 | resolve()
185 | }, 3000)
186 | )
187 | return {
188 | response: {
189 | code: 200,
190 | message: `${new Date().toLocaleString()} open light success`
191 | }
192 | }
193 | }
194 | },
195 | state: initialState
196 | }
197 |
198 | export default model
199 |
200 | // You can use these types when use Class Components.
201 | // type ConsumerActionsType = getConsumerActionsType
202 | // type ConsumerType = { actions: ConsumerActionsType; state: StateType }
203 | // type ActionType = ConsumerActionsType
204 | // export { ConsumerType, StateType, ActionType }
205 | ```
206 |
207 | [⇧ back to top](#table-of-contents)
208 |
209 | ### Model Register
210 |
211 | react-model keeps the application state and actions in separate private stores. So you need to register them if you want to use them as the public models.
212 |
213 | `model/index.ts`
214 |
215 | ```typescript
216 | import { Model } from 'react-model'
217 | import Home from '../model/home'
218 | import Shared from '../model/shared'
219 |
220 | const models = { Home, Shared }
221 |
222 | export const { getInitialState, useStore, getState, actions, subscribe, unsubscribe } = Model(models)
223 | ```
224 |
225 | [⇧ back to top](#table-of-contents)
226 |
227 | ### useStore
228 |
229 | The functional component in React ^16.8.0 can use Hooks to connect the global store.
230 | The actions returned from useStore can invoke dom changes.
231 |
232 | The execution of actions returned by useStore will invoke the rerender of current component first.
233 |
234 | It's the only difference between the actions returned by useStore and actions now.
235 |
236 | ```tsx
237 | import React from 'react'
238 | import { useStore } from '../index'
239 |
240 | // CSR
241 | export default () => {
242 | const [state, actions] = useStore('Home')
243 | const [sharedState, sharedActions] = useStore('Shared')
244 |
245 | return (
246 |
247 | Home model value: {JSON.stringify(state)}
248 | Shared model value: {JSON.stringify(sharedState)}
249 |
250 |
253 |
254 |
255 |
256 | )
257 | }
258 | ```
259 |
260 | optional solution on huge dataset (example: TodoList(10000+ Todos)):
261 |
262 | 1. use useStore on the subComponents which need it.
263 | 2. use useStore selector. (version >= v4.0.0-rc.0)
264 |
265 | [advance example with 1000 todo items](https://codesandbox.io/s/react-model-v4-todomvc-oxyij)
266 |
267 | [⇧ back to top](#table-of-contents)
268 |
269 | ### getState
270 |
271 | Key Point: [State variable not updating in useEffect callback](https://github.com/facebook/react/issues/14066)
272 |
273 | To solve it, we provide a way to get the current state of model: getState
274 |
275 | Note: the getState method cannot invoke the dom changes automatically by itself.
276 |
277 | > Hint: The state returned should only be used as readonly
278 |
279 | ```jsx
280 | import { useStore, getState } from '../model/index'
281 |
282 | const BasicHook = () => {
283 | const [state, actions] = useStore('Counter')
284 | useEffect(() => {
285 | console.log('some mounted actions from BasicHooks')
286 | return () =>
287 | console.log(
288 | `Basic Hooks unmounted, current Counter state: ${JSON.stringify(
289 | getState('Counter')
290 | )}`
291 | )
292 | }, [])
293 | return (
294 | <>
295 | state: {JSON.stringify(state)}
296 | >
297 | )
298 | }
299 | ```
300 |
301 | [⇧ back to top](#table-of-contents)
302 |
303 | ### actions
304 |
305 | You can call other models' actions with actions api
306 |
307 | actions can be used in both class components and functional components.
308 |
309 | ```js
310 | import { actions } from './index'
311 |
312 | const model = {
313 | state: {},
314 | actions: {
315 | crossModelCall: () => {
316 | actions.Shared.changeTheme('dark')
317 | actions.Counter.increment(9)
318 | }
319 | }
320 | }
321 |
322 | export default model
323 | ```
324 |
325 | [⇧ back to top](#table-of-contents)
326 |
327 | ### subscribe
328 |
329 | subscribe(storeName, actions, callback) run the callback when the specific actions executed.
330 |
331 | ```typescript
332 | import { subscribe, unsubscribe } from './index'
333 |
334 | const callback = () => {
335 | const user = getState('User')
336 | localStorage.setItem('user_id', user.id)
337 | }
338 |
339 | // subscribe action
340 | subscribe('User', 'login', callback)
341 | // subscribe actions
342 | subscribe('User', ['login', 'logout'], callback)
343 | // unsubscribe the observer of some actions
344 | unsubscribe('User', 'login') // only logout will run callback now
345 | ```
346 |
347 | [⇧ back to top](#table-of-contents)
348 |
349 | ## Advance Concept
350 |
351 | ### immutable Actions
352 |
353 | The actions use [immer](https://github.com/mweststrate/immer) produce API to modify the Store. You can return a producer in action.
354 |
355 | Using function as return value can make your code cleaner when you modify the deep nested value.
356 |
357 | TypeScript Example
358 |
359 | ```ts
360 | // StateType and ActionsParamType definition
361 | // ...
362 |
363 | const model: ModelType = {
364 | actions: {
365 | increment: async (params, { state: s }) => {
366 | // return (state: typeof s) => { // TypeScript < 3.9
367 | return state => {
368 | state.counter += params || 1
369 | }
370 | },
371 | decrease: params => s => {
372 | s.counter += params || 1
373 | }
374 | }
375 | }
376 |
377 | export default model
378 | ```
379 |
380 | JavaScript Example
381 |
382 | ```js
383 | const Model = {
384 | actions: {
385 | increment: async (params) => {
386 | return state => {
387 | state.counter += params || 1
388 | }
389 | }
390 | }
391 | }
392 | ```
393 |
394 | [⇧ back to top](#table-of-contents)
395 |
396 | ### SSR with Next.js
397 |
398 |
399 | Store: shared.ts
400 |
401 |
402 | ```ts
403 | const initialState = {
404 | counter: 0
405 | }
406 |
407 | const model: ModelType = {
408 | actions: {
409 | increment: (params, { state }) => {
410 | return {
411 | counter: state.counter + (params || 1)
412 | }
413 | }
414 | },
415 | // Provide for SSR
416 | asyncState: async context => {
417 | await waitFor(4000)
418 | return { counter: 500 }
419 | },
420 | state: initialState
421 | }
422 |
423 | export default model
424 | ```
425 |
426 |
427 |
428 |
429 |
430 | Global Config: _app.tsx
431 |
432 |
433 |
434 | ```tsx
435 | import { models, getInitialState, Models } from '../model/index'
436 |
437 | let persistModel: any
438 |
439 | interface ModelsProps {
440 | initialModels: Models
441 | persistModel: Models
442 | }
443 |
444 | const MyApp = (props: ModelsProps) => {
445 | if ((process as any).browser) {
446 | // First come in: initialModels
447 | // After that: persistModel
448 | persistModel = props.persistModel || Model(models, props.initialModels)
449 | }
450 | const { Component, pageProps, router } = props
451 | return (
452 |
453 |
454 |
455 | )
456 | }
457 |
458 | MyApp.getInitialProps = async (context: NextAppContext) => {
459 | if (!(process as any).browser) {
460 | const initialModels = context.Component.getInitialProps
461 | ? await context.Component.getInitialProps(context.ctx)
462 | await getInitialState(undefined, { isServer: true }) // get all model initialState
463 | // : await getInitialState({ modelName: 'Home' }, { isServer: true }) // get Home initialState only
464 | // : await getInitialState({ modelName: ['Home', 'Todo'] }, { isServer: true }) // get multi initialState
465 | // : await getInitialState({ data }, { isServer: true }) // You can also pass some public data as asyncData params.
466 | return { initialModels }
467 | } else {
468 | return { persistModel }
469 | }
470 | }
471 | ```
472 |
473 |
474 |
475 |
476 | Page: hooks/index.tsx
477 |
478 |
479 | ```tsx
480 | import { useStore, getState } from '../index'
481 | export default () => {
482 | const [state, actions] = useStore('Home')
483 | const [sharedState, sharedActions] = useStore('Shared')
484 |
485 | return (
486 |
487 | Home model value: {JSON.stringify(state)}
488 | Shared model value: {JSON.stringify(sharedState)}
489 |
495 | )
496 | }
497 | ```
498 |
499 |
500 |
501 |
502 | Single Page Config: benchmark.tsx
503 |
504 |
505 | ```tsx
506 | // ...
507 | Benchmark.getInitialProps = async () => {
508 | return await getInitialState({ modelName: 'Todo' }, { isServer: true })
509 | }
510 | ```
511 |
512 |
513 |
514 | [⇧ back to top](#table-of-contents)
515 |
516 | ### Middleware
517 |
518 | We always want to try catch all the actions, add common request params, connect Redux devtools and so on. We Provide the middleware pattern for developer to register their own Middleware to satisfy the specific requirement.
519 |
520 | ```tsx
521 | // Under the hood
522 | const tryCatch: Middleware<{}> = async (context, restMiddlewares) => {
523 | const { next } = context
524 | await next(restMiddlewares).catch((e: any) => console.log(e))
525 | }
526 |
527 | // ...
528 |
529 | let actionMiddlewares = [
530 | tryCatch,
531 | getNewState,
532 | setNewState,
533 | stateUpdater,
534 | communicator,
535 | devToolsListener
536 | ]
537 |
538 | // ...
539 | // How we execute an action
540 | const consumerAction = (action: Action) => async (params: any) => {
541 | const context: Context = {
542 | modelName,
543 | setState,
544 | actionName: action.name,
545 | next: () => {},
546 | newState: null,
547 | params,
548 | consumerActions,
549 | action
550 | }
551 | await applyMiddlewares(actionMiddlewares, context)
552 | }
553 |
554 | // ...
555 |
556 | export { ... , actionMiddlewares}
557 | ```
558 |
559 | ⚙️ You can override the actionMiddlewares and insert your middleware to specific position
560 |
561 | [⇧ back to top](#table-of-contents)
562 |
563 | ### Expand Context
564 |
565 | ```typescript
566 | const ExtCounter: ModelType<
567 | { name: string }, // State Type
568 | { ext: undefined }, // ActionParamsType
569 | { name: string } // ExtContextType
570 | > = {
571 | actions: {
572 | // { state, action } => { state, action, [name] }
573 | ext: (_, { name }) => {
574 | return { name }
575 | }
576 | },
577 | state: { name: '' }
578 | }
579 |
580 | const { useStore } = Model(ExtCounter, { name: 'test' })
581 | // state.name = ''
582 | const [state, actions] = useStore()
583 | // ...
584 | actions.ext()
585 | // state.name => 'test'
586 | ```
587 |
588 | [⇧ back to top](#table-of-contents)
589 |
590 | ## Other Concept required by Class Component
591 |
592 | ### Provider
593 |
594 | The global state standalone can not effect the react class components, we need to provide the state to react root component.
595 |
596 | ```jsx
597 | import { PureComponent } from 'react'
598 | import { Provider } from 'react-model'
599 |
600 | class App extends PureComponent {
601 | render() {
602 | return (
603 |
604 |
605 |
606 | )
607 | }
608 | }
609 | ```
610 |
611 | [⇧ back to top](#table-of-contents)
612 |
613 | ### connect
614 |
615 | We can use the Provider state with connect.
616 |
617 |
618 | Javascript decorator version
619 |
620 |
621 | ```jsx
622 | import React, { PureComponent } from 'react'
623 | import { Provider, connect } from 'react-model'
624 |
625 | const mapProps = ({ light, counter }) => ({
626 | lightStatus: light ? 'open' : 'close',
627 | counter
628 | }) // You can map the props in connect.
629 |
630 | @connect(
631 | 'Home',
632 | mapProps
633 | )
634 | export default class JSCounter extends PureComponent {
635 | render() {
636 | const { state, actions } = this.props
637 | return (
638 | <>
639 |
states - {JSON.stringify(state)}
640 |
641 |
642 | >
643 | )
644 | }
645 | }
646 | ```
647 |
648 |
649 |
650 |
651 |
652 | TypeScript Version
653 |
654 |
655 | ```tsx
656 | import React, { PureComponent } from 'react'
657 | import { Provider, connect } from 'react-model'
658 | import { StateType, ActionType } from '../model/home'
659 |
660 | const mapProps = ({ light, counter, response }: StateType) => ({
661 | lightStatus: light ? 'open' : 'close',
662 | counter,
663 | response
664 | })
665 |
666 | type RType = ReturnType
667 |
668 | class TSCounter extends PureComponent<
669 | { state: RType } & { actions: ActionType }
670 | > {
671 | render() {
672 | const { state, actions } = this.props
673 | return (
674 | <>
675 | TS Counter
676 | states - {JSON.stringify(state)}
677 |
678 |
679 |
680 | message: {JSON.stringify(state.response)}
681 | >
682 | )
683 | }
684 | }
685 |
686 | export default connect(
687 | 'Home',
688 | mapProps
689 | )(TSCounter)
690 | ```
691 |
692 |
693 |
694 | [⇧ back to top](#table-of-contents)
695 |
696 | ## FAQ
697 |
698 | ### Migrate from 4.0.x to 4.1.x
699 |
700 | 1. replace Model with createStore
701 |
702 | `counter.ts`
703 |
704 | ```ts
705 | import { createStore } from 'react-model'
706 | // Remove typedef below
707 | // type CounterState = {
708 | // count: number
709 | // }
710 |
711 | // type CounterActionParams = {
712 | // increment: number
713 | // }
714 |
715 | // v4.0.x model
716 | const Counter: ModelType<
717 | CounterState,
718 | CounterActionParams
719 | > = {
720 | actions: {
721 | increment: (params) => {
722 | return (state) => {
723 | state.count += params
724 | }
725 | }
726 | },
727 | state: { count: 0 }
728 | }
729 |
730 | // v4.1.x
731 | const Counter = createStore(() => {
732 | const [state, setState] = useModel({ count: 0 })
733 | const actions = {
734 | increment: (params) => {
735 | setState((state) => {
736 | state.count += params
737 | })
738 | }
739 | }
740 | return [state, actions] as const
741 | })
742 |
743 | export default Counter
744 | ```
745 |
746 | 2. Remove Counter from model registry
747 |
748 | ```ts
749 | const models = {
750 | // Counter
751 | Shared
752 | }
753 |
754 | export const { getInitialState, useStore, getState, actions, subscribe, unsubscribe } = Model(models)
755 | ```
756 |
757 | 3. update useStore calls in components
758 |
759 | ```tsx
760 | // import { useStore } from 'models'
761 | import Counter from 'models/counter'
762 |
763 | const Component = () => {
764 | // const [state, actions] = useStore('Counter')
765 | const [state, actions] = Counter.useStore()
766 | }
767 | ```
768 |
769 | ### Migrate from 3.1.x to 4.x.x
770 |
771 | 1. remove Model wrapper
772 |
773 | `sub-model.ts`
774 | ```ts
775 | // 3.1.x
776 | export default Model(model)
777 | // 4.x.x
778 | export default model
779 | ```
780 |
781 | `models.ts`
782 | ```ts
783 | import Sub from './sub-model'
784 | export default Model({ Sub })
785 | ```
786 |
787 | 2. use selector to replace depActions
788 |
789 | `Shared.ts`
790 | ```ts
791 | interface State {
792 | counter: number
793 | enable: boolean
794 | }
795 |
796 | interface ActionParams {
797 | add: number
798 | switch: undefined
799 | }
800 |
801 | const model: ModelType = {
802 | state: {
803 | counter: 1
804 | enable: false
805 | },
806 | actions: {
807 | add: (payload) => state => {
808 | state.counter += payload
809 | },
810 | switch: () => state => {
811 | state.enable = !state.enable
812 | }
813 | }
814 | }
815 | ```
816 |
817 | ```ts
818 | const Component = () => {
819 | // 3.1.x, Component rerender when add action is invoked
820 | const [counter] = useStore('Shared', ['add'])
821 | // 4.x.x, Component rerender when counter value diff
822 | const [counter] = useStore('Shared', state => state.counter)
823 | }
824 | ```
825 |
826 | ### How can I disable the console debugger
827 |
828 |
829 | ```typescript
830 | import { middlewares } from 'react-model'
831 | // Find the index of middleware
832 |
833 | // Disable all actions' log
834 | middlewares.config.logger.enable = false
835 | // Disable logs from specific type of actions
836 | middlewares.config.logger.enable = ({ actionName }) => ['increment'].indexOf(actionName) !== -1
837 | ```
838 |
839 | [⇧ back to top](#table-of-contents)
840 |
841 | ### How can I add custom middleware
842 |
843 | ```typescript
844 | import { actionMiddlewares, middlewares, Model } from 'react-model'
845 | import { sendLog } from 'utils/log'
846 | import Home from '../model/home'
847 | import Shared from '../model/shared'
848 |
849 | // custom middleware
850 | const ErrorHandler: Middleware = async (context, restMiddlewares) => {
851 | const { next } = context
852 | await next(restMiddlewares).catch((e: Error) => sendLog(e))
853 | }
854 |
855 | // Find the index of middleware
856 | const getNewStateMiddlewareIndex = actionMiddlewares.indexOf(
857 | middlewares.getNewState
858 | )
859 |
860 | // Replace it
861 | actionMiddlewares.splice(getNewStateMiddlewareIndex, 0, ErrorHandler)
862 |
863 | const stores = { Home, Shared }
864 |
865 | export default Model(stores)
866 | ```
867 |
868 | [⇧ back to top](#table-of-contents)
869 |
870 | #### How can I make persist models
871 |
872 | ```typescript
873 | import { actionMiddlewares, Model } from 'react-model'
874 | import Example from 'models/example'
875 |
876 | // Example, not recommend to use on production directly without consideration
877 | // Write current State to localStorage after action finish
878 | const persistMiddleware: Middleware = async (context, restMiddlewares) => {
879 | localStorage.setItem('__REACT_MODEL__', JSON.stringify(context.Global.State))
880 | await context.next(restMiddlewares)
881 | }
882 |
883 | // Use on all models
884 | actionMiddlewares.push(persistMiddleware)
885 | Model({ Example }, JSON.parse(localStorage.getItem('__REACT_MODEL__')))
886 |
887 | // Use on single model
888 | const model = {
889 | state: JSON.parse(localStorage.getItem('__REACT_MODEL__'))['you model name']
890 | actions: { ... },
891 | middlewares: [...actionMiddlewares, persistMiddleware]
892 | }
893 |
894 |
895 | ```
896 |
897 | [⇧ back to top](#table-of-contents)
898 |
899 | ### How can I deal with local state
900 |
901 | What should I do to make every Counter hold there own model? 🤔
902 |
903 | ```tsx
904 | class App extends Component {
905 | render() {
906 | return (
907 |
908 |
909 |
910 |
911 |
912 | )
913 | }
914 | }
915 | ```
916 |
917 |
918 | Counter model
919 |
920 |
921 | ```ts
922 | interface State {
923 | count: number
924 | }
925 |
926 | interface ActionParams {
927 | increment: number
928 | }
929 |
930 | const model: ModelType = {
931 | state: {
932 | count: 0
933 | },
934 | actions: {
935 | increment: payload => {
936 | // immer.module.js:972 Uncaught (in promise) Error: An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft
937 | // Not allowed
938 | // return state => (state.count += payload)
939 | return state => {
940 | state.count += payload
941 | }
942 | }
943 | }
944 | }
945 |
946 | ```
947 |
948 |
949 |
950 |
951 |
952 | Counter.tsx
953 |
954 |
955 | ```tsx
956 |
957 | const Counter = () => {
958 | const [{ useStore }] = useState(() => Model(model))
959 | const [state, actions] = useStore()
960 | return (
961 |
962 |
{state.count}
963 |
964 |
965 | )
966 | }
967 |
968 | export default Counter
969 | ```
970 |
971 |
972 |
973 |
974 | [⇧ back to top](#table-of-contents)
975 |
976 | ### How can I deal with huge dataset / circular dataset
977 |
978 | [Immer assumes your state to be a unidirectional tree. That is, no object should appear twice in the tree, there should be no circular references.](https://immerjs.github.io/immer/pitfalls#immer-only-supports-unidirectional-trees)
979 |
980 | Immer freezes everything recursively, for large data objects that won't be changed in the future this might be over-kill, in that case it can be more efficient to shallowly pre-freeze data using the freeze utility.
981 |
982 | ```ts
983 | import { freeze } from 'immer'
984 |
985 | export const ExpensiveModel: ModelType = {
986 | state: {
987 | moduleList: []
988 | },
989 | actions: {
990 | setPreFreezedDataset: () => {
991 | const optimizedDataset = freeze(hugeDataset)
992 | return { moduleList: optimizedDataset }
993 | }
994 | }
995 | }
996 | ```
997 |
998 | ### actions throw error from immer.module.js
999 |
1000 | ```
1001 | immer.module.js:972 Uncaught (in promise) Error: An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft
1002 | ```
1003 |
1004 | How to fix:
1005 |
1006 | ```tsx
1007 | actions: {
1008 | increment: payload => {
1009 | // Not allowed
1010 | // return state => (state.count += payload)
1011 | return state => {
1012 | state.count += payload
1013 | }
1014 | }
1015 | }
1016 | ```
1017 |
1018 | [⇧ back to top](#table-of-contents)
1019 |
1020 | ### How can I customize each model's middlewares?
1021 |
1022 | You can customize each model's middlewares.
1023 |
1024 | ```typescript
1025 | import { actionMiddlewares, Model } from 'react-model'
1026 | const delayMiddleware: Middleware = async (context, restMiddlewares) => {
1027 | await timeout(1000, {})
1028 | context.next(restMiddlewares)
1029 | }
1030 |
1031 | const nextCounterModel: ModelType = {
1032 | actions: {
1033 | add: num => {
1034 | return state => {
1035 | state.count += num
1036 | }
1037 | },
1038 | increment: async (num, { actions }) => {
1039 | actions.add(num)
1040 | await timeout(300, {})
1041 | }
1042 | },
1043 | // You can define the custom middlewares here
1044 | middlewares: [delayMiddleware, ...actionMiddlewares],
1045 | state: {
1046 | count: 0
1047 | }
1048 | }
1049 |
1050 | export default Model(nextCounterModel)
1051 | ```
1052 |
1053 | [⇧ back to top](#table-of-contents)
1054 |
--------------------------------------------------------------------------------
/__test__/Model/error.multi.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | // @ts-ignore
4 | import { ErrorModel as EM } from './errorModel'
5 | import { Model } from '../../src'
6 |
7 | describe('useStore', () => {
8 | test('create by single model error definition', async () => {
9 | let state: any
10 | let actions: any
11 | let count = 0
12 | const ErrorModel = Model(EM)
13 | // @ts-ignore
14 | const { useStore, subscribe, unsubscribe } = Model({ ErrorModel })
15 | renderHook(() => {
16 | ;[state, actions] = useStore('ErrorModel')
17 | })
18 | expect(actions).toEqual({})
19 | expect(actions.increment).toBe(undefined)
20 | // await actions.increment(3)
21 | expect(state).toEqual({})
22 | // test subscribe
23 | // @ts-ignore
24 | subscribe('increment', () => (count += 1))
25 | expect(count).toBe(0)
26 | expect(state.count).toBe(undefined)
27 | // test unsubscribe
28 | // @ts-ignore
29 | unsubscribe('increment')
30 | expect(actions).toEqual({})
31 | expect(state.count).toBe(undefined)
32 | expect(count).toBe(0)
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/__test__/Model/error.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | // @ts-ignore
4 | import { ErrorModel } from './errorModel'
5 | import { Model } from '../../src'
6 |
7 | describe('useStore', () => {
8 | test('create by single model definition', async () => {
9 | let state: any
10 | let actions: any
11 | let count = 0
12 | // @ts-ignore
13 | const { useStore, subscribe, unsubscribe } = Model(ErrorModel)
14 | renderHook(() => {
15 | ;[state, actions] = useStore()
16 | })
17 | expect(state).toEqual({})
18 | expect(actions.increment).toBe(undefined)
19 | // await actions.increment(3)
20 | expect(state).toEqual({})
21 | // test subscribe
22 | subscribe('increment', () => (count += 1))
23 | expect(actions).toEqual({})
24 | expect(count).toBe(0)
25 | expect(state.count).toBe(undefined)
26 | // test unsubscribe
27 | unsubscribe('increment')
28 | expect(actions).toEqual({})
29 | expect(state.count).toBe(undefined)
30 | expect(count).toBe(0)
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/__test__/Model/errorModel.ts:
--------------------------------------------------------------------------------
1 | // Use to simulate a error model.js file
2 | export const ErrorModel: any = {
3 | actions: {
4 | // @ts-ignore
5 | add: (params, { state }) => {
6 | return {
7 | count: state.count + params
8 | }
9 | },
10 | // @ts-ignore
11 | addCaller: (_, { actions }) => {
12 | actions.add(5)
13 | },
14 | // @ts-ignore
15 | increment: params => {
16 | // @ts-ignore
17 | return state => {
18 | state.count += params
19 | }
20 | },
21 | state: { count: 0 }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/__test__/Model/extContext.multi.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { ExtCounter } from '..'
4 | import { Model } from '../../src'
5 |
6 | describe('useStore', () => {
7 | test('models with extContext', async () => {
8 | // Multiple Model with Context
9 | let testState: any
10 | let testActions: any
11 | let extState: any
12 | let extActions: any
13 | // @ts-ignore
14 | const Test = Model(ExtCounter, { name: 'test' })
15 | // @ts-ignore
16 | const Ext = Model(ExtCounter, { name: 'ext' })
17 | const { useStore } = Model({ Test, Ext })
18 | renderHook(() => {
19 | ;[testState, testActions] = useStore('Test')
20 | ;[extState, extActions] = useStore('Ext')
21 | })
22 | expect(testState).toEqual({ name: '' })
23 | await testActions.ext()
24 | expect(testState).toEqual({ name: 'test' })
25 | expect(extState).toEqual({ name: '' })
26 | await extActions.ext()
27 | expect(extState).toEqual({ name: 'ext' })
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/__test__/Model/extContext.spec.single.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { ExtCounter } from '..'
4 | import { Model } from '../../src'
5 |
6 | describe('useStore', () => {
7 | test('models with extContext', async () => {
8 | // Single Model extContext
9 | let state: any
10 | let actions: any
11 | const { useStore: u } = Model(ExtCounter, { name: 'test' })
12 | renderHook(() => {
13 | ;[state, actions] = u()
14 | })
15 | expect(state).toEqual({ name: '' })
16 | await actions.ext()
17 | expect(state).toEqual({ name: 'test' })
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/__test__/Model/mixed.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { NextCounter } from '..'
4 | import { Model } from '../../src'
5 |
6 | describe('useStore', () => {
7 | test('create by single model definition', async () => {
8 | let state: any, nextState: any
9 | let actions: any, nextActions: any
10 | let count = 0
11 | let nextCount = 0
12 | const Home = Model(NextCounter)
13 | const { useStore, subscribe, unsubscribe } = Model({ Home, NextCounter })
14 | renderHook(() => {
15 | ;[state, actions] = useStore('Home')
16 | ;[nextState, nextActions] = useStore('NextCounter')
17 | })
18 |
19 | // Home
20 | expect(state).toEqual({ count: 0 })
21 | await actions.increment(3)
22 | expect(state).toEqual({ count: 3 })
23 | // test subscribe
24 | subscribe('Home', 'increment', () => (count += 1))
25 | await actions.increment(4)
26 | expect(count).toBe(1)
27 | expect(state.count).toBe(7)
28 | // test unsubscribe
29 | unsubscribe('Home', 'increment')
30 | await actions.increment(3)
31 | expect(state.count).toBe(10)
32 | expect(count).toBe(1)
33 |
34 | // NextCounter
35 | expect(nextState).toEqual({ count: 0 })
36 | await nextActions.increment(3)
37 | expect(nextState).toEqual({ count: 3 })
38 | // test subscribe
39 | subscribe('NextCounter', 'increment', () => (nextCount += 1))
40 | await nextActions.increment(4)
41 | expect(nextCount).toBe(1)
42 | expect(nextState.count).toBe(7)
43 | // test unsubscribe
44 | unsubscribe('NextCounter', 'increment')
45 | await nextActions.increment(3)
46 | expect(nextState.count).toBe(10)
47 | expect(nextCount).toBe(1)
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/__test__/Model/return.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook, act } from '@testing-library/react-hooks'
3 | import { RetTester } from '..'
4 | import { Model } from '../../src'
5 |
6 | describe('action return value', () => {
7 | test('return object value', async () => {
8 | let actions: any
9 | const { useStore } = Model(RetTester)
10 | renderHook(() => {
11 | ;[, actions] = useStore()
12 | })
13 | await act(async () => {
14 | const retVal = await actions.add(5)
15 | expect(retVal).toEqual({ count: 5 })
16 | const retVal_2 = await actions.add(5)
17 | expect(retVal).toEqual({ count: 5 })
18 | expect(retVal_2).toEqual({ count: 10 })
19 | })
20 | })
21 |
22 | test('return promise value', async () => {
23 | let actions: any
24 | const { useStore } = Model(RetTester)
25 | renderHook(() => {
26 | ;[, actions] = useStore()
27 | })
28 | await act(async () => {
29 | const retVal = await actions.asyncAdd(5)
30 | expect(retVal).toEqual({ count: 5 })
31 | const retVal_2 = await actions.asyncAdd(5)
32 | expect(retVal).toEqual({ count: 5 })
33 | expect(retVal_2).toEqual({ count: 10 })
34 | })
35 | })
36 |
37 | test('return produce function', async () => {
38 | const asyncPrototype = Object.getPrototypeOf(async () => {})
39 | const isAsync = (input: unknown) => {
40 | return Object.getPrototypeOf(input) === asyncPrototype
41 | }
42 | let actions: any
43 | const { useStore } = Model(RetTester)
44 | renderHook(() => {
45 | ;[, actions] = useStore()
46 | })
47 | await act(async () => {
48 | const retVal = await actions.produceAdd(5)
49 | expect(isAsync(retVal)).toBe(true)
50 | const retVal_2 = await actions.produceAdd(5)
51 | expect(isAsync(retVal)).toBe(true)
52 | expect(isAsync(retVal_2)).toBe(true)
53 | })
54 | })
55 |
56 | test('return async produce function', async () => {
57 | const asyncPrototype = Object.getPrototypeOf(async () => {})
58 | const isAsync = (input: unknown) => {
59 | return Object.getPrototypeOf(input) === asyncPrototype
60 | }
61 | let actions: any
62 | const { useStore } = Model(RetTester)
63 | renderHook(() => {
64 | ;[, actions] = useStore()
65 | })
66 | await act(async () => {
67 | const retVal = await actions.asyncProduceAdd(5)
68 | expect(isAsync(retVal)).toBe(true)
69 | const retVal_2 = await actions.asyncProduceAdd(5)
70 | expect(isAsync(retVal)).toBe(true)
71 | expect(isAsync(retVal_2)).toBe(true)
72 | })
73 | })
74 |
75 | test('return action', async () => {
76 | let actions: any
77 | const { useStore } = Model(RetTester)
78 | renderHook(() => {
79 | ;[, actions] = useStore()
80 | })
81 | await act(async () => {
82 | const retVal = await actions.hocAdd(5)
83 | expect(retVal).toEqual({ count: 5 })
84 | const retVal_2 = await actions.hocAdd(5)
85 | expect(retVal).toEqual({ count: 5 })
86 | expect(retVal_2).toEqual({ count: 10 })
87 | })
88 | })
89 |
90 | test('return async action', async () => {
91 | let actions: any
92 | const { useStore } = Model(RetTester)
93 | renderHook(() => {
94 | ;[, actions] = useStore()
95 | })
96 | await act(async () => {
97 | const retVal = await actions.asyncHocAdd(5)
98 | expect(retVal).toEqual({ count: 5 })
99 | const retVal_2 = await actions.asyncHocAdd(5)
100 | expect(retVal).toEqual({ count: 5 })
101 | expect(retVal_2).toEqual({ count: 10 })
102 | })
103 | })
104 | })
105 |
--------------------------------------------------------------------------------
/__test__/Model/same-name.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { NextCounter } from '..'
4 | import { Model } from '../../src'
5 |
6 | describe('useStore', () => {
7 | test('create by single model definition', async () => {
8 | let state: any
9 | let actions: any
10 | let mirrorState: any
11 | let mirrorActions: any
12 | let count = 0
13 | const { useStore, subscribe, unsubscribe, getState } = Model({
14 | NextCounter
15 | })
16 | const {
17 | useStore: useMirrorStore,
18 | subscribe: mirrorSubscribe,
19 | unsubscribe: mirrorUnSubscribe,
20 | getState: getMirrorState
21 | } = Model({ NextCounter })
22 | renderHook(() => {
23 | ;[state, actions] = useStore('NextCounter')
24 | ;[mirrorState, mirrorActions] = useMirrorStore('NextCounter')
25 | })
26 | expect(state).toEqual({ count: 0 })
27 | expect(mirrorState).toEqual({ count: 0 })
28 |
29 | mirrorSubscribe('NextCounter', 'increment', () => (count += 1))
30 |
31 | await actions.increment(3)
32 | expect(state).toEqual({ count: 3 })
33 | expect(mirrorState).toEqual({ count: 0 })
34 | expect(count).toBe(0)
35 |
36 | await mirrorActions.increment(3)
37 | expect(state).toEqual({ count: 3 })
38 | expect(mirrorState).toEqual({ count: 3 })
39 | expect(count).toBe(1)
40 |
41 | // test subscribe
42 | subscribe('NextCounter', 'increment', () => (count += 1))
43 | await actions.increment(4)
44 | expect(count).toBe(2)
45 | expect(state.count).toBe(7)
46 | expect(mirrorState.count).toBe(3)
47 | expect(getState('NextCounter').count).toBe(7)
48 | expect(getMirrorState('NextCounter').count).toBe(3)
49 |
50 | // test unsubscribe
51 | unsubscribe('NextCounter', 'increment')
52 | mirrorUnSubscribe('NextCounter', 'increment')
53 | await actions.increment(3)
54 | expect(state.count).toBe(10)
55 | expect(mirrorState.count).toBe(3)
56 | expect(getState('NextCounter').count).toBe(10)
57 | expect(getMirrorState('NextCounter').count).toBe(3)
58 | expect(count).toBe(2)
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/__test__/Model/share-setState.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { State } from '..'
4 | import { Model } from '../../src'
5 |
6 | describe('common used case', () => {
7 | test('create by single model with common setState', async () => {
8 | let state: any
9 | let actions: any
10 | let failed = false
11 | const { useStore, getState } = Model(State)
12 | renderHook(() => {
13 | ;[state, actions] = useStore()
14 | })
15 | expect(state).toEqual({ xxx: '', yyy: -1 })
16 | await actions.setState({ yyy: 3 })
17 | expect(state).toEqual({ xxx: '', yyy: 3 })
18 | // @ts-ignore
19 | await actions.setState(state => {
20 | state.yyy = 1
21 | })
22 | expect(state.yyy).toBe(1)
23 | expect(getState().xxx).toBe("")
24 | expect(getState().yyy).toBe(1)
25 |
26 | expect(failed).toBe(false)
27 |
28 | try {
29 | // BAD USE CASE
30 | // 1. use both return value and produce modifier
31 | // @ts-ignore
32 | await actions.setState((state) => {
33 | state.xxx = "xxx"
34 | return { yyy: 10 }
35 | })
36 | // nothing changed
37 | expect(state.yyy).toBe(1)
38 | expect(getState().xxx).toBe("")
39 | } catch {
40 | failed = true
41 | }
42 |
43 | expect(failed).toBe(true)
44 |
45 | // 2. return partial value in produce func
46 | // @ts-ignore
47 | await actions.setState(() => {
48 | return { yyy: 10 }
49 | })
50 | // key xxx will be dropped
51 | expect(state.yyy).toBe(10)
52 | expect(getState().xxx).toBe(undefined)
53 |
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/__test__/Model/single.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { NextCounter } from '..'
4 | import { Model } from '../../src'
5 |
6 | describe('useStore', () => {
7 | test('create by single model definition', async () => {
8 | let state: any
9 | let actions: any
10 | let count = 0
11 | const { useStore, subscribe, unsubscribe, getState } = Model(NextCounter)
12 | renderHook(() => {
13 | ;[state, actions] = useStore()
14 | })
15 | expect(state).toEqual({ count: 0 })
16 | await actions.increment(3)
17 | expect(state).toEqual({ count: 3 })
18 | // test subscribe
19 | subscribe('increment', () => (count += 1))
20 | await actions.increment(4)
21 | expect(count).toBe(1)
22 | expect(state.count).toBe(7)
23 | expect(getState().count).toBe(7)
24 | // test unsubscribe
25 | unsubscribe('increment')
26 | await actions.increment(3)
27 | expect(state.count).toBe(10)
28 | expect(getState().count).toBe(10)
29 | expect(count).toBe(1)
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/__test__/SSR/index.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Model } from '../../src'
3 | import { SSRCounter } from '..'
4 |
5 | describe('asyncState', () => {
6 | test('return default initial state from asyncState', async () => {
7 | const { getInitialState } = Model({
8 | WrappedSSRCounter: Model(SSRCounter),
9 | SSRCounter
10 | })
11 | const initialModels = await getInitialState(undefined, { isServer: true })
12 | // const state = getState('AsyncCounter')
13 | expect(initialModels['SSRCounter'].count).toBe(1)
14 | expect(initialModels['SSRCounter'].clientKey).toBe(undefined)
15 | expect(initialModels['WrappedSSRCounter'].count).toBe(1)
16 | expect(initialModels['WrappedSSRCounter'].clientKey).toBe(undefined)
17 |
18 | // Simulate Client Side
19 | const { getState } = Model(
20 | { WrappedSSRCounter: Model(SSRCounter), SSRCounter },
21 | initialModels
22 | )
23 | expect(initialModels['SSRCounter'].count).toBe(1)
24 | expect(initialModels['WrappedSSRCounter'].count).toBe(1)
25 | expect(getState('SSRCounter').clientKey).toBe('unused')
26 | expect(getState('WrappedSSRCounter').clientKey).toBe('unused')
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/__test__/actions/actions.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Model } from '../../src'
4 | import { ActionsTester } from '../index'
5 |
6 | describe('actions', () => {
7 | test('get actions from Model', async () => {
8 | const { actions, useStore } = Model({ ActionsTester })
9 | let state: any
10 | renderHook(() => {
11 | ;[state] = useStore('ActionsTester')
12 | })
13 | await actions.ActionsTester.getData()
14 | expect(state.data).toEqual({ counter: 1000 })
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/__test__/actions/getActions.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { ActionsTester } from '../index'
4 | import { Model } from '../../src'
5 |
6 | describe('actions', () => {
7 | test('call actions in action', async () => {
8 | const { getActions, getState } = Model({ ActionsTester })
9 | let state: any
10 | let actions: any
11 | renderHook(() => {
12 | actions = getActions('ActionsTester')
13 | })
14 | await actions.getData()
15 | state = getState('ActionsTester')
16 | expect(state.data).toEqual({ counter: 1000 })
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/__test__/actions/unmount.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { ActionsTester } from '../index'
4 | import { Model } from '../../src'
5 |
6 | describe('actions', () => {
7 | test('call actions in action', async () => {
8 | const { useStore } = Model({ ActionsTester })
9 | let state: any
10 | let actions: any
11 | const { unmount } = renderHook(() => {
12 | ;[state, actions] = useStore('ActionsTester')
13 | })
14 | await actions.getData()
15 | unmount()
16 | expect(state.data).toEqual({ counter: 1000 })
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/__test__/actions/useStore.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { ActionsTester } from '../index'
4 | import { Model } from '../../src'
5 |
6 | describe('actions', () => {
7 | test('call actions in action', async () => {
8 | const { useStore } = Model({ ActionsTester })
9 | let state: any
10 | let actions: any
11 | renderHook(() => {
12 | ;[state, actions] = useStore('ActionsTester')
13 | })
14 | await actions.getData()
15 | expect(state.data).toEqual({ counter: 1000 })
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/__test__/asyncState.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Model } from '../src'
3 | import { AsyncCounter, AsyncNull } from '.'
4 |
5 | describe('asyncState', () => {
6 | test('asyncState accept context params with error modelName', async () => {
7 | const { getInitialState, getState } = Model({ AsyncCounter })
8 | await getInitialState({ count: 2, modelName: 'Async' })
9 | const state = getState('AsyncCounter')
10 | expect(state.count).toBe(0)
11 | })
12 | test('return default initial state from asyncState', async () => {
13 | const { getState, getInitialState } = Model({ AsyncCounter })
14 | await getInitialState()
15 | const state = getState('AsyncCounter')
16 | expect(state.count).toBe(1)
17 | })
18 | test('asyncState accept context params', async () => {
19 | const { getInitialState, getState } = Model({ AsyncCounter })
20 | await getInitialState({ count: 2 })
21 | const state = getState('AsyncCounter')
22 | expect(state.count).toBe(2)
23 | })
24 | test('asyncState accept context params with modelName', async () => {
25 | const { getInitialState, getState } = Model({ AsyncCounter })
26 | await getInitialState({ count: 3, modelName: 'AsyncCounter' })
27 | const state = getState('AsyncCounter')
28 | expect(state.count).toBe(3)
29 | })
30 | test('asyncState work without asyncState', async () => {
31 | const { getInitialState, getState } = Model({ AsyncNull })
32 | await getInitialState()
33 | const state = getState('AsyncNull')
34 | expect(state.count).toBe(0)
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/__test__/class/class.spec.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import * as React from 'react'
3 | import { Model, Provider, connect } from '../../src'
4 | import { Counter } from '../index'
5 | import { render, fireEvent } from '@testing-library/react'
6 | import { timeout } from '../../src/helper'
7 |
8 | const Button = connect(
9 | 'Counter',
10 | (props: any) => props
11 | )(
12 | class extends React.PureComponent {
13 | render() {
14 | const { state, actions } = this.props
15 | return (
16 |
23 | )
24 | }
25 | }
26 | )
27 |
28 | describe('class component', () => {
29 | test('Provider', () => {
30 | Model({ Counter })
31 | const { container } = render(
32 |
33 |
34 |
35 | )
36 | const button = container.firstChild
37 | expect(button!.textContent).toBe('0')
38 | })
39 | test('Consumer', async () => {
40 | Model({ Counter })
41 | const { container } = render(
42 |
43 |
44 |
45 | )
46 | const button: any = container.firstChild
47 | expect(button!.textContent).toBe('0')
48 | fireEvent.click(button)
49 | await timeout(100, {}) // Wait Consumer rerender
50 | expect(button!.textContent).toBe('3')
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/__test__/class/communicator.spec.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import * as React from 'react'
3 | import { Model, Provider, connect } from '../../src'
4 | import { Counter } from '../index'
5 | import { render, fireEvent } from '@testing-library/react'
6 | import { renderHook } from '@testing-library/react-hooks'
7 | import { timeout } from '../../src/helper'
8 |
9 | const Button = connect(
10 | 'Counter',
11 | (props: any) => props
12 | )(
13 | class extends React.PureComponent {
14 | render() {
15 | const { state, actions } = this.props
16 | return (
17 |
24 | )
25 | }
26 | }
27 | )
28 |
29 | describe('class component', () => {
30 | test('communicator', async () => {
31 | let state: any
32 | const { useStore } = Model({ Counter })
33 | renderHook(() => {
34 | ;[state] = useStore('Counter')
35 | })
36 | const { container } = render(
37 |
38 |
39 |
40 | )
41 | const button: any = container.firstChild
42 | expect(button!.textContent).toBe('0')
43 | fireEvent.click(button)
44 | await timeout(100, {}) // Wait Consumer rerender
45 | expect(button!.textContent).toBe('3')
46 | expect(state.count).toBe(3)
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/__test__/class/mapActions.spec.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import * as React from 'react'
3 | import { Model, Provider, connect } from '../../src'
4 | import { Counter } from '..'
5 | import { render } from '@testing-library/react'
6 |
7 | const Button = connect(
8 | 'Counter',
9 | (props: any) => props,
10 | (actions: any) => {
11 | REACT_MODEL: actions
12 | }
13 | )(
14 | class extends React.PureComponent {
15 | render() {
16 | const { state, REACT_MODEL, buttonName = '' } = this.props
17 | return (
18 |
26 | )
27 | }
28 | }
29 | )
30 |
31 | describe('class component', () => {
32 | test('render props', () => {
33 | Model({ Counter })
34 | const { container } = render(
35 |
36 |
37 |
38 | )
39 | const button = container.firstChild
40 | expect(button!.textContent).toBe('button0')
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/__test__/class/renderProps.spec.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import * as React from 'react'
3 | import { Model, Provider, connect } from '../../src'
4 | import { Counter } from '..'
5 | import { render } from '@testing-library/react'
6 |
7 | const Button = connect(
8 | 'Counter',
9 | (props: any) => props
10 | )(
11 | class extends React.PureComponent {
12 | render() {
13 | const { state, actions, buttonName = '' } = this.props
14 | return (
15 |
23 | )
24 | }
25 | }
26 | )
27 |
28 | describe('class component', () => {
29 | test('render props', () => {
30 | Model({ Counter })
31 | const { container } = render(
32 |
33 |
34 |
35 | )
36 | const button = container.firstChild
37 | expect(button!.textContent).toBe('button0')
38 | })
39 | })
40 |
--------------------------------------------------------------------------------
/__test__/connect.spec.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import * as React from 'react'
3 | import { Model, Provider, connect } from '../src'
4 | import { Counter, Theme } from './'
5 | import { render, fireEvent } from '@testing-library/react'
6 | import { timeout } from '../src/helper'
7 |
8 | const Button = connect('Counter', (props: any) => ({ counter: props }))(
9 | connect('Theme', (props: any) => ({ theme: props }))(
10 | class extends React.Component {
11 | render() {
12 | const { state, actions } = this.props
13 | return (
14 |
23 | )
24 | }
25 | }
26 | )
27 | )
28 |
29 | describe('class component', () => {
30 | test('multi connect', async () => {
31 | Model({ Counter, Theme })
32 | const { container } = render(
33 |
34 |
35 |
36 | )
37 | const button: any = container.firstChild
38 | expect(button!.textContent).toBe('0dark')
39 | fireEvent.click(button)
40 | await timeout(100, {}) // Wait Consumer rerender
41 | expect(button!.textContent).toBe('3light')
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/__test__/disable-dubugger.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | ///
3 | import { Model, middlewares } from '../src'
4 | import { Counter } from '.'
5 | import { renderHook } from '@testing-library/react-hooks'
6 |
7 | middlewares.config.logger.enable = ({ actionName }) =>
8 | ['increment'].indexOf(actionName) !== -1
9 |
10 | describe('PubSub', () => {
11 | test('run callback when specific action run', async () => {
12 | let actions: any
13 | let count = 0
14 | const { useStore, subscribe } = Model({ Counter })
15 | subscribe('Counter', 'increment', () => (count += 1))
16 | subscribe('Counter', 'add', () => (count += 10))
17 | renderHook(() => {
18 | ;[, actions] = useStore('Counter')
19 | })
20 | await actions.increment()
21 | await actions.add(1)
22 | await actions.increment()
23 | await actions.increment()
24 | expect(count).toBe(13)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/__test__/dubugger.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | console.group = undefined
3 | ///
4 | import { Model, middlewares } from '../src'
5 | import { Counter } from '.'
6 | import { renderHook } from '@testing-library/react-hooks'
7 |
8 | describe('PubSub', () => {
9 | test('run callback when specific action run', async () => {
10 | middlewares.config.logger.enable = true
11 | let actions: any
12 | let count = 0
13 | const { useStore, subscribe } = Model({ Counter })
14 | subscribe('Counter', 'increment', () => (count += 1))
15 | subscribe('Counter', 'add', () => (count += 10))
16 | renderHook(() => {
17 | ;[, actions] = useStore('Counter')
18 | })
19 | await actions.increment()
20 | await actions.add(1)
21 | await actions.increment()
22 | await actions.increment()
23 | expect(count).toBe(13)
24 | middlewares.config.logger.enable = false
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/__test__/getActions.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Counter } from '.'
4 | import { Model } from '../src'
5 |
6 | describe('useStore', () => {
7 | test('return default initial values', () => {
8 | let state
9 | const { useStore } = Model({ Counter })
10 | renderHook(() => {
11 | ;[state] = useStore('Counter')
12 | })
13 | expect(state).toEqual({ count: 0 })
14 | })
15 | test('consumer actions return function', async () => {
16 | let state: any
17 | let actions: any
18 | const { useStore, getActions } = Model({ Counter })
19 | renderHook(() => {
20 | ;[state] = useStore('Counter')
21 | actions = getActions('Counter')
22 | })
23 | await actions.increment(3)
24 | expect(state).toEqual({ count: 3 })
25 | await actions.increment(4)
26 | expect(state.count).toBe(7)
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/__test__/getState.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Model } from '../src'
3 | import { Counter } from '.'
4 |
5 | describe('getState', () => {
6 | test('return default initial state', () => {
7 | let state
8 | const { getState } = Model({ Counter })
9 | state = getState('Counter')
10 | expect(state.count).toBe(0)
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/__test__/index.d.ts:
--------------------------------------------------------------------------------
1 | type CounterState = {
2 | count: number
3 | }
4 |
5 | type RetState = {
6 | count: number
7 | extra: string
8 | }
9 |
10 | type SSRCounterState = {
11 | count: number
12 | clientKey: string
13 | }
14 |
15 | type ExtState = {
16 | name: string
17 | }
18 |
19 | type ThemeState = {
20 | theme: 'dark' | 'light'
21 | }
22 |
23 | type ActionTesterState = {
24 | response: { data: Object }
25 | data: Object
26 | }
27 |
28 | type CounterActionParams = {
29 | increment: number
30 | }
31 |
32 | type ExtActionParams = {
33 | ext: undefined
34 | }
35 |
36 | type NextCounterActionParams = {
37 | increment: number
38 | add: number
39 | }
40 |
41 | type RetActionParams = {
42 | add: number
43 | asyncAdd: number
44 | produceAdd: number
45 | asyncProduceAdd: number
46 | hocAdd: number
47 | asyncHocAdd: number
48 | }
49 |
50 | type ExtraActionParams = {
51 | add: number
52 | addCaller: undefined
53 | }
54 |
55 | type ThemeActionParams = {
56 | changeTheme: 'dark' | 'light'
57 | }
58 |
59 | type ActionTesterParams = {
60 | get: undefined
61 | parse: undefined
62 | getData: undefined
63 | }
64 |
--------------------------------------------------------------------------------
/__test__/index.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | import { Model } from '../src'
4 | import { timeout } from '../src/helper'
5 | import { actionMiddlewares } from '../src/middlewares'
6 | import { freeze } from 'immer'
7 | import { name } from 'faker'
8 |
9 | export const ActionsTester: ModelType = {
10 | actions: {
11 | get: async () => {
12 | const response = await timeout(9, { code: 0, data: { counter: 1000 } })
13 | return { response }
14 | },
15 | getData: async (_, { actions }) => {
16 | await actions.get()
17 | actions.parse()
18 | },
19 | parse: () => {
20 | return (state) => {
21 | state.data = state.response.data
22 | }
23 | }
24 | },
25 | state: {
26 | data: {},
27 | response: {
28 | data: {}
29 | }
30 | }
31 | }
32 |
33 | export const Counter: ModelType<
34 | CounterState,
35 | CounterActionParams & ExtraActionParams
36 | > = {
37 | actions: {
38 | add: (params, { state }) => {
39 | return {
40 | count: state.count + params
41 | }
42 | },
43 | addCaller: (_, { actions }) => {
44 | actions.add(5)
45 | },
46 | increment: (params) => {
47 | return (state) => {
48 | state.count += params
49 | }
50 | }
51 | },
52 | state: { count: 0 }
53 | }
54 |
55 | // v3.0
56 | export const NextCounter: ModelType<
57 | CounterState,
58 | CounterActionParams & ExtraActionParams
59 | > = {
60 | actions: {
61 | add: (params, { state }) => {
62 | return {
63 | count: state.count + params
64 | }
65 | },
66 | addCaller: (_, { actions }) => {
67 | actions.add(5)
68 | },
69 | increment: (params) => {
70 | return (state) => {
71 | state.count += params
72 | }
73 | }
74 | },
75 | state: { count: 0 }
76 | }
77 |
78 | // common used case
79 | interface CommonState {
80 | xxx: string
81 | yyy: number
82 | }
83 |
84 | interface CommonActionParams {
85 | setState: Partial | ProduceFunc>
86 | }
87 |
88 | export const State: ModelType = {
89 | state: {
90 | xxx: '',
91 | yyy: -1
92 | },
93 | actions: {
94 | setState: async (payload) => payload
95 | }
96 | }
97 |
98 | export const ExtCounter: ModelType<
99 | ExtState,
100 | ExtActionParams,
101 | { name: string }
102 | > = {
103 | actions: {
104 | ext: (_, { name }) => {
105 | return {
106 | name
107 | }
108 | }
109 | },
110 | state: { name: '' }
111 | }
112 |
113 | export const Theme: ModelType = {
114 | actions: {
115 | changeTheme: (_, { state }) => ({
116 | theme: state.theme === 'dark' ? 'light' : 'dark'
117 | })
118 | },
119 | state: {
120 | theme: 'dark'
121 | }
122 | }
123 |
124 | export const AsyncCounter: ModelType = {
125 | actions: {
126 | increment: (params) => {
127 | return (state) => {
128 | state.count += params
129 | }
130 | }
131 | },
132 | asyncState: async (context: { count?: number }) => ({
133 | count: context ? context.count || 1 : 1
134 | }),
135 | state: { count: 0 }
136 | }
137 |
138 | export const SSRCounter: ModelType = {
139 | actions: {
140 | increment: (params) => {
141 | return (state) => {
142 | state.count += params
143 | }
144 | }
145 | },
146 | asyncState: async (context: { count?: number }) => ({
147 | count: context ? context.count || 1 : 1
148 | }),
149 | state: { count: 0, clientKey: 'unused' }
150 | }
151 |
152 | export const AsyncNull: ModelType = {
153 | actions: {
154 | increment: (params) => {
155 | return (state) => {
156 | state.count += params
157 | }
158 | }
159 | },
160 | state: { count: 0 }
161 | }
162 |
163 | const timeoutCounter: ModelType = {
164 | actions: {
165 | increment: async (params, { state: _ }) => {
166 | await timeout(4000, {})
167 | return (state: typeof _) => {
168 | state.count += params
169 | }
170 | }
171 | },
172 | asyncState: async () => ({
173 | count: 1
174 | }),
175 | state: { count: 0 }
176 | }
177 |
178 | export const RetTester: ModelType = {
179 | state: {
180 | count: 0,
181 | extra: 'extra'
182 | },
183 | actions: {
184 | add: (num, { state }) => {
185 | return { count: state.count + num }
186 | },
187 | asyncAdd: async (num, { state }) => {
188 | await timeout(300, {})
189 | return { count: state.count + num }
190 | },
191 | produceAdd: (num) => {
192 | return (state) => {
193 | state.count += num
194 | }
195 | },
196 | asyncProduceAdd: async (num) => {
197 | await timeout(300, {})
198 | return (state) => {
199 | state.count += num
200 | }
201 | },
202 | hocAdd: (num, { actions }) => {
203 | return actions.add(num)
204 | },
205 | asyncHocAdd: async (num, { actions }) => {
206 | await timeout(100, {})
207 | return actions.add(num)
208 | }
209 | }
210 | }
211 |
212 | export const TimeoutCounter = Model(timeoutCounter)
213 |
214 | export const ErrorCounter: ModelType = {
215 | actions: {
216 | increment: async () => {
217 | throw 'error'
218 | }
219 | },
220 | state: { count: 0 }
221 | }
222 |
223 | const delayMiddleware: Middleware = async (context, restMiddlewares) => {
224 | await timeout(1000, {})
225 | context.next(restMiddlewares)
226 | }
227 |
228 | export const NextCounterModel: ModelType<
229 | CounterState,
230 | NextCounterActionParams
231 | > = {
232 | actions: {
233 | add: (num) => {
234 | return (state) => {
235 | state.count += num
236 | }
237 | },
238 | increment: async (num, { actions }) => {
239 | actions.add(num)
240 | await timeout(300, {})
241 | }
242 | },
243 | middlewares: [delayMiddleware, ...actionMiddlewares],
244 | state: {
245 | count: 0
246 | }
247 | }
248 |
249 | interface ExpensiveState {
250 | moduleList: any[]
251 | }
252 |
253 | interface ExpensiveActionParams {
254 | setState: undefined
255 | setPreFreezedDataset: undefined
256 | }
257 |
258 | const hugeDataset = []
259 |
260 | // Create circular data
261 | console.time('create data')
262 | Array.from(Array(20000).keys()).forEach((_, idx) => {
263 | // const obj = makeDeepFakeData(100)
264 | hugeDataset.push({
265 | idx,
266 | name: name.findName(),
267 | parent: hugeDataset
268 | })
269 | })
270 | console.timeEnd('create data')
271 |
272 | export const ExpensiveModel: ModelType<
273 | ExpensiveState,
274 | ExpensiveActionParams
275 | > = {
276 | state: {
277 | moduleList: []
278 | },
279 | actions: {
280 | setState: () => {
281 | return { moduleList: hugeDataset }
282 | },
283 | setPreFreezedDataset: () => {
284 | const optimizedDataset = freeze(hugeDataset)
285 | return { moduleList: optimizedDataset }
286 | }
287 | }
288 | }
289 |
--------------------------------------------------------------------------------
/__test__/lane/lane.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook, act } from '@testing-library/react-hooks'
3 | import { createStore, useModel, Model } from '../../src'
4 |
5 | describe('lane model', () => {
6 | test('single model', async () => {
7 | const { useStore } = createStore(() => {
8 | const [count, setCount] = useModel(1)
9 | return { count, setCount }
10 | })
11 | let renderTimes = 0
12 | const { result } = renderHook(() => {
13 | const { count, setCount } = useStore()
14 | renderTimes += 1
15 | return { renderTimes, count, setCount }
16 | })
17 |
18 | act(() => {
19 | expect(result.current.renderTimes).toEqual(1)
20 | expect(result.current.count).toBe(1)
21 | })
22 |
23 | act(() => {
24 | result.current.setCount(5)
25 | })
26 |
27 | act(() => {
28 | expect(renderTimes).toEqual(2)
29 | expect(result.current.count).toBe(5)
30 | })
31 | })
32 |
33 | test('create store with namespace', async () => {
34 | const { useStore } = Model({})
35 | createStore('Shared', () => {
36 | const [count, setCount] = useModel(1)
37 | return { count, setCount }
38 | })
39 |
40 | let renderTimes = 0
41 | const { result } = renderHook(() => {
42 | // @ts-ignore
43 | const { count, setCount } = useStore('Shared')
44 | console.group('count: ', count)
45 | console.group('setCount: ', setCount)
46 | renderTimes += 1
47 | return { renderTimes, count, setCount }
48 | })
49 | act(() => {
50 | expect(renderTimes).toEqual(1)
51 | expect(result.current.count).toBe(1)
52 | })
53 |
54 | act(() => {
55 | // @ts-ignore
56 | result.current.setCount(5)
57 | })
58 |
59 | act(() => {
60 | expect(renderTimes).toEqual(2)
61 | expect(result.current.count).toBe(5)
62 | })
63 | })
64 |
65 | test('subscribe model', () => {
66 | let subscribeTimes = 0
67 | const { subscribe, unsubscribe, getState } = createStore(() => {
68 | const [count, setCount] = useModel(1)
69 | return { count, setCount }
70 | })
71 |
72 | const callback_1 = () => {
73 | subscribeTimes += 1
74 | }
75 |
76 | const callback_2 = () => {
77 | subscribeTimes += 1
78 | }
79 |
80 | subscribe(callback_1)
81 | subscribe(callback_2)
82 |
83 | act(() => {
84 | expect(subscribeTimes).toEqual(0)
85 | })
86 |
87 | act(() => {
88 | getState().setCount(5)
89 | })
90 |
91 | act(() => {
92 | expect(subscribeTimes).toEqual(2)
93 | expect(getState().count).toBe(5)
94 | })
95 |
96 | unsubscribe(callback_1)
97 |
98 | act(() => {
99 | getState().setCount(15)
100 | })
101 |
102 | act(() => {
103 | expect(subscribeTimes).toEqual(3)
104 | expect(getState().count).toBe(15)
105 | })
106 | })
107 |
108 | test('pass function to useModel ', async () => {
109 | const { useStore } = createStore(() => {
110 | const [count, setCount] = useModel(() => 1)
111 | return { count, setCount }
112 | })
113 | let renderTimes = 0
114 | const { result } = renderHook(() => {
115 | const { count, setCount } = useStore()
116 | renderTimes += 1
117 | return { renderTimes, count, setCount }
118 | })
119 |
120 | act(() => {
121 | expect(renderTimes).toEqual(1)
122 | expect(result.current.count).toBe(1)
123 | })
124 |
125 | act(() => {
126 | result.current.setCount((count) => count + 1)
127 | })
128 |
129 | act(() => {
130 | expect(renderTimes).toEqual(2)
131 | expect(result.current.count).toBe(2)
132 | })
133 | })
134 |
135 | test('false value can be accepted', async () => {
136 | const { useStore } = createStore(() => {
137 | const [count, setCount] = useModel(true)
138 | return { count, setCount }
139 | })
140 |
141 | let renderTimes = 0
142 | const { result, rerender } = renderHook(() => {
143 | const { count, setCount } = useStore()
144 | renderTimes += 1
145 | return { renderTimes, count, setCount }
146 | })
147 | act(() => {
148 | expect(renderTimes).toEqual(1)
149 | expect(result.current.count).toBe(true)
150 | })
151 |
152 | act(() => {
153 | result.current.setCount(false)
154 | })
155 |
156 | act(() => {
157 | expect(renderTimes).toEqual(2)
158 | expect(result.current.count).toBe(false)
159 | })
160 |
161 | act(() => {
162 | rerender()
163 | })
164 |
165 | act(() => {
166 | expect(renderTimes).toEqual(3)
167 | expect(result.current.count).toBe(false)
168 | })
169 | })
170 |
171 | test('array value is protected', async () => {
172 | const { useStore } = createStore(() => {
173 | const [list, setList] = useModel(>[])
174 | return { list, setList }
175 | })
176 |
177 | let renderTimes = 0
178 | const { result, rerender } = renderHook(() => {
179 | const { list, setList } = useStore()
180 | renderTimes += 1
181 | return { renderTimes, list, setList }
182 | })
183 | act(() => {
184 | expect(renderTimes).toEqual(1)
185 | expect(result.current.list.constructor.name).toBe('Array')
186 | })
187 |
188 | act(() => {
189 | result.current.setList([1, 2])
190 | })
191 |
192 | act(() => {
193 | expect(renderTimes).toEqual(2)
194 | expect(result.current.list.constructor.name).toBe('Array')
195 | expect(result.current.list[0]).toBe(1)
196 | expect(result.current.list[1]).toBe(2)
197 | })
198 |
199 | act(() => {
200 | rerender()
201 | })
202 |
203 | act(() => {
204 | expect(renderTimes).toEqual(3)
205 | expect(result.current.list.constructor.name).toBe('Array')
206 | expect(result.current.list[0]).toBe(1)
207 | expect(result.current.list[1]).toBe(2)
208 | })
209 | })
210 |
211 | test('use getStore outside react lifecycle', async () => {
212 | const { useStore, getStore } = createStore(() => {
213 | const [count, setCount] = useModel(1)
214 | return { count, setCount }
215 | })
216 | let renderTimes = 0
217 |
218 | act(() => {
219 | expect(getStore()).toEqual(undefined)
220 | })
221 |
222 | const { result } = renderHook(() => {
223 | const { count, setCount } = useStore()
224 | renderTimes += 1
225 | return { renderTimes, count, setCount }
226 | })
227 |
228 | act(() => {
229 | expect(result.current.renderTimes).toEqual(1)
230 | expect(result.current.count).toBe(1)
231 | expect(getStore()?.count).toBe(1)
232 | })
233 |
234 | act(() => {
235 | result.current.setCount(5)
236 | })
237 |
238 | act(() => {
239 | expect(renderTimes).toEqual(2)
240 | expect(result.current.count).toBe(5)
241 | expect(getStore()?.count).toBe(5)
242 | })
243 | })
244 |
245 | test('multiple models', async () => {
246 | const { useStore } = createStore(() => {
247 | const [count, setCount] = useModel(1)
248 | const [name, setName] = useModel('Jane')
249 | return { count, name, setName, setCount }
250 | })
251 | let renderTimes = 0
252 | const { result } = renderHook(() => {
253 | const { count, setCount, name, setName } = useStore()
254 | renderTimes += 1
255 | return { renderTimes, count, setCount, name, setName }
256 | })
257 | act(() => {
258 | expect(renderTimes).toEqual(1)
259 | expect(result.current.count).toBe(1)
260 | })
261 |
262 | act(() => {
263 | result.current.setCount(5)
264 | })
265 |
266 | act(() => {
267 | expect(renderTimes).toEqual(2)
268 | expect(result.current.count).toBe(5)
269 | expect(result.current.name).toBe('Jane')
270 | })
271 |
272 | act(() => {
273 | result.current.setName('Bob')
274 | })
275 |
276 | act(() => {
277 | expect(renderTimes).toEqual(3)
278 | expect(result.current.name).toBe('Bob')
279 | expect(result.current.count).toBe(5)
280 | })
281 | })
282 |
283 | test('multiple stores', async () => {
284 | const { useStore } = createStore(() => {
285 | const [count, setCount] = useModel(1)
286 |
287 | return { count, setCount }
288 | })
289 |
290 | const { useStore: useOtherStore } = createStore(() => {
291 | const [name, setName] = useModel('Jane')
292 | return { name, setName }
293 | })
294 | let renderTimes = 0
295 | const { result } = renderHook(() => {
296 | const { count, setCount } = useStore()
297 | const { name, setName } = useOtherStore()
298 | renderTimes += 1
299 | return { renderTimes, count, setCount, name, setName }
300 | })
301 |
302 | act(() => {
303 | expect(renderTimes).toEqual(1)
304 | expect(result.current.count).toBe(1)
305 | })
306 |
307 | act(() => {
308 | result.current.setCount(5)
309 | })
310 |
311 | act(() => {
312 | expect(renderTimes).toEqual(2)
313 | expect(result.current.count).toBe(5)
314 | expect(result.current.name).toBe('Jane')
315 | })
316 |
317 | act(() => {
318 | result.current.setName('Bob')
319 | })
320 |
321 | act(() => {
322 | expect(renderTimes).toEqual(3)
323 | expect(result.current.name).toBe('Bob')
324 | expect(result.current.count).toBe(5)
325 | })
326 | })
327 |
328 | test('share single model between components', async () => {
329 | const { useStore } = createStore(() => {
330 | const [count, setCount] = useModel(1)
331 | return { count, setCount }
332 | })
333 | let renderTimes = 0
334 | const { result } = renderHook(() => {
335 | const { count, setCount } = useStore()
336 | renderTimes += 1
337 | return { renderTimes, count, setCount }
338 | })
339 |
340 | const { result: mirrorResult } = renderHook(() => {
341 | const { count, setCount } = useStore()
342 | renderTimes += 1
343 | return { renderTimes, count, setCount }
344 | })
345 | act(() => {
346 | expect(renderTimes).toEqual(2)
347 | expect(mirrorResult.current.count).toBe(1)
348 | })
349 |
350 | act(() => {
351 | result.current.setCount(5)
352 | })
353 |
354 | act(() => {
355 | expect(renderTimes).toEqual(4)
356 | expect(mirrorResult.current.count).toBe(5)
357 | })
358 | })
359 |
360 | test('complex case', async () => {
361 | const { useStore, getState } = createStore(() => {
362 | const [count, setCount] = useModel(1)
363 | const [name, setName] = useModel('Jane')
364 | return { count, setCount, name, setName }
365 | })
366 | const { useStore: useOtherStore } = createStore(() => {
367 | const [data, setData] = useModel({ status: 'UNKNOWN' })
368 | return { data, setData }
369 | })
370 | const { useStore: useOnce } = createStore(() => {
371 | const [status, set] = useModel(false)
372 | return { status, set }
373 | })
374 | let renderTimes = 0
375 | const { result } = renderHook(() => {
376 | const { count, setCount } = useStore()
377 | const { setData } = useOtherStore()
378 | renderTimes += 1
379 | return { renderTimes, count, setCount, setData }
380 | })
381 |
382 | const { result: mirrorResult } = renderHook(() => {
383 | const { setName, name } = useStore()
384 | const { data } = useOtherStore()
385 | const { status, set } = useOnce()
386 | renderTimes += 1
387 | return { renderTimes, data, setName, name, status, set }
388 | })
389 | act(() => {
390 | expect(renderTimes).toEqual(2)
391 | expect(mirrorResult.current.name).toBe('Jane')
392 | })
393 |
394 | act(() => {
395 | result.current.setData({ status: 'SUCCESS' })
396 | })
397 |
398 | // Both component will rerender
399 | // TODO: Only rerender second FC
400 | act(() => {
401 | expect(renderTimes).toEqual(4)
402 | expect(mirrorResult.current.data).toEqual({ status: 'SUCCESS' })
403 | })
404 |
405 | act(() => {
406 | mirrorResult.current.setName('Bob')
407 | })
408 |
409 | act(() => {
410 | expect(renderTimes).toEqual(6)
411 | expect(mirrorResult.current.name).toBe('Bob')
412 | expect(mirrorResult.current.status).toBe(false)
413 | })
414 |
415 | act(() => {
416 | mirrorResult.current.set(true)
417 | })
418 |
419 | act(() => {
420 | expect(renderTimes).toEqual(7)
421 | expect(mirrorResult.current.status).toBe(true)
422 | })
423 |
424 | act(() => {
425 | mirrorResult.current.setName('Jane')
426 | })
427 |
428 | act(() => {
429 | expect(renderTimes).toEqual(9)
430 | expect(mirrorResult.current.name).toBe('Jane')
431 | expect(mirrorResult.current.status).toBe(true)
432 | expect(getState().name).toBe('Jane')
433 | expect(getState().count).toBe(1)
434 | })
435 | })
436 | })
437 |
--------------------------------------------------------------------------------
/__test__/lane/migrate.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook, act } from '@testing-library/react-hooks'
3 | import { createStore, useModel } from '../../src'
4 |
5 | describe('migrate test', async () => {
6 | test('migrate from v4.0.x', async () => {
7 | const store = createStore(() => {
8 | const [state, setState] = useModel({ count: 0, otherKey: 'key' })
9 | const actions = {
10 | add: (params: number) => {
11 | return setState({
12 | count: state.count + params
13 | })
14 | },
15 | addCaller: () => {
16 | actions.add(5)
17 | },
18 | increment: (params: number) => {
19 | return setState((state) => {
20 | state.count += params
21 | })
22 | }
23 | }
24 | return [state, actions] as const
25 | })
26 |
27 | let renderTimes = 0
28 |
29 | const { result } = renderHook(() => {
30 | renderTimes += 1
31 | const [state, actions] = store.useStore()
32 | return { state, renderTimes, actions }
33 | })
34 |
35 | act(() => {
36 | expect(result.current.renderTimes).toEqual(1)
37 | expect(result.current.state.count).toBe(0)
38 | })
39 |
40 | act(() => {
41 | result.current.actions.addCaller()
42 | })
43 |
44 | act(() => {
45 | expect(renderTimes).toEqual(2)
46 | expect(result.current.state.count).toBe(5)
47 | expect(result.current.state.otherKey).toBe('key')
48 | })
49 |
50 | act(() => {
51 | result.current.actions.increment(5)
52 | })
53 |
54 |
55 | act(() => {
56 | expect(renderTimes).toEqual(3)
57 | expect(result.current.state.count).toBe(10)
58 | })
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/__test__/lane/react.spec.ts:
--------------------------------------------------------------------------------
1 | ///
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
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
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('getStore with useEffect inside FC', async () => {
136 | const useCount = () => {
137 | const [count, setCount] = useState(1)
138 | return { count, setCount }
139 | }
140 | const { useStore, getStore } = createStore(useCount)
141 | let renderTimes = 0
142 | let validEffectTimes = 0
143 | const { result } = renderHook(() => {
144 | const { count: lastCount } = getStore() || { count: undefined }
145 | const { count, setCount } = useStore()
146 | useEffect(() => {
147 | const { count: cachedCount } = getStore() || { count: undefined }
148 | console.error(lastCount, ' ', count, ' ', cachedCount)
149 | if (cachedCount === count && count !== lastCount) {
150 | validEffectTimes += 1
151 | }
152 | })
153 | renderTimes += 1
154 | console.error('validEffectTimes: ', validEffectTimes)
155 | return { renderTimes, count, setCount, validEffectTimes }
156 | })
157 | act(() => {
158 | expect(result.current.renderTimes).toEqual(1)
159 | expect(validEffectTimes).toEqual(1)
160 | // effect is next tick
161 | expect(result.current.validEffectTimes).toEqual(0)
162 | expect(result.current.count).toBe(1)
163 | })
164 |
165 | act(() => {
166 | result.current.setCount(5)
167 | })
168 |
169 | act(() => {
170 | expect(renderTimes).toEqual(2)
171 | expect(validEffectTimes).toEqual(2)
172 | // effect is next tick
173 | expect(result.current.validEffectTimes).toEqual(1)
174 | expect(result.current.count).toBe(5)
175 | })
176 | })
177 |
178 | test('combine useState and useStore', async () => {
179 | const useCount = () => {
180 | // useState create local state
181 | const [count, setCount] = useState(1)
182 | // useModel create shared state
183 | const [name, setName] = useModel('Jane')
184 | return { count, setCount, name, setName }
185 | }
186 | const { useStore } = createStore(useCount)
187 | let renderTimes = 0
188 | const { result } = renderHook(() => {
189 | const { count, setCount, name, setName } = useStore()
190 | renderTimes += 1
191 | return { renderTimes, count, setCount, name, setName }
192 | })
193 | const { result: otherResult } = renderHook(() => {
194 | const { count, setCount, name } = useStore()
195 | renderTimes += 1
196 | return { renderTimes, count, setCount, name }
197 | })
198 |
199 | await act(async () => {
200 | expect(result.current.renderTimes).toBe(1)
201 | expect(otherResult.current.renderTimes).toBe(2)
202 | expect(result.current.count).toBe(1)
203 | })
204 |
205 | await act(() => {
206 | otherResult.current.setCount(5)
207 | })
208 |
209 | await act(() => {
210 | expect(result.current.renderTimes).toEqual(1)
211 | expect(otherResult.current.renderTimes).toEqual(3)
212 | expect(otherResult.current.count).toBe(5)
213 | expect(result.current.count).toBe(1)
214 | })
215 |
216 | await act(() => {
217 | result.current.setCount(50)
218 | })
219 |
220 | await act(() => {
221 | expect(result.current.renderTimes).toEqual(4)
222 | expect(otherResult.current.renderTimes).toEqual(3)
223 | expect(otherResult.current.count).toBe(5)
224 | expect(result.current.count).toBe(50)
225 | })
226 |
227 | await act(async () => {
228 | result.current.setName('Bob')
229 | })
230 |
231 | await act(() => {
232 | expect(result.current.renderTimes).toEqual(5)
233 | expect(otherResult.current.renderTimes).toEqual(6)
234 | expect(result.current.name).toBe('Bob')
235 | expect(otherResult.current.name).toBe('Bob')
236 | })
237 | })
238 | })
239 |
--------------------------------------------------------------------------------
/__test__/middlewares/commuicator.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Model } from '../../src'
4 | import { Counter } from '..'
5 |
6 | describe('', () => {
7 | test('communicator', async () => {
8 | let stateFirst: any, stateSecond: any
9 | let actionsFirst: any, actionsSecond: any
10 | const { useStore, getActions } = Model({ Counter })
11 | const actions = getActions('Counter')
12 | renderHook(() => {
13 | ;[stateFirst, actionsFirst] = useStore('Counter')
14 | })
15 | renderHook(() => {
16 | ;[stateSecond, actionsSecond] = useStore('Counter')
17 | })
18 | expect(stateFirst.count).toBe(0)
19 | expect(stateSecond.count).toBe(0)
20 | await actionsFirst.increment(3)
21 | expect(stateFirst.count).toBe(3)
22 | expect(stateSecond.count).toBe(3)
23 | await actionsSecond.increment(4)
24 | expect(stateFirst.count).toBe(7)
25 | expect(stateSecond.count).toBe(7)
26 | await actions.increment(4)
27 | expect(stateFirst.count).toBe(11)
28 | expect(stateSecond.count).toBe(11)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/__test__/middlewares/devToolsListener.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ;(window as any).__REDUX_DEVTOOLS_EXTENSION__ = {
3 | connect: () => {},
4 | send: () => {}
5 | }
6 | import { renderHook, act } from '@testing-library/react-hooks'
7 | import { Model, middlewares, createStore, useModel } from '../../src'
8 | import { Counter } from '..'
9 |
10 | middlewares.config.devtools.enable = true
11 |
12 | describe('withDevTools', () => {
13 | test("won't break the behavior without DevTools", async () => {
14 | let state: any
15 | let actions: any
16 | const { useStore } = Model({ Counter })
17 | renderHook(() => {
18 | ;[state, actions] = useStore('Counter')
19 | })
20 | expect(state).toEqual({ count: 0 })
21 | await actions.increment(3)
22 | expect(state.count).toBe(3)
23 | })
24 |
25 | test('support createStore', () => {
26 | const { useStore } = createStore(() => {
27 | const [count, setCount] = useModel(1)
28 | return { count, setCount }
29 | })
30 | let renderTimes = 0
31 | const { result } = renderHook(() => {
32 | const { count, setCount } = useStore()
33 | renderTimes += 1
34 | return { renderTimes, count, setCount }
35 | })
36 |
37 | act(() => {
38 | expect(result.current.renderTimes).toEqual(1)
39 | expect(result.current.count).toBe(1)
40 | })
41 |
42 | act(() => {
43 | result.current.setCount(5)
44 | })
45 |
46 | act(() => {
47 | expect(renderTimes).toEqual(2)
48 | expect(result.current.count).toBe(5)
49 | })
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/__test__/middlewares/getNewStateWithCache.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Model, middlewares, actionMiddlewares } from '../../src'
3 | import { renderHook } from '@testing-library/react-hooks'
4 | import { TimeoutCounter } from '..'
5 |
6 | describe('getNewStateWithCache: ', () => {
7 | test('cache when timeout', async () => {
8 | let state: any, actions: any
9 | const cacheMiddlewareIndex = actionMiddlewares.indexOf(
10 | middlewares.getNewState
11 | )
12 | actionMiddlewares[cacheMiddlewareIndex] = middlewares.getNewStateWithCache(
13 | 3000
14 | )
15 | const { useStore } = Model({ TimeoutCounter })
16 | renderHook(() => {
17 | ;[state, actions] = useStore('TimeoutCounter')
18 | })
19 | await actions.increment(3)
20 | expect(state).toEqual({ count: 0 })
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/__test__/middlewares/middlewareConfig.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Model } from '../../src'
4 | import { Counter } from '../'
5 |
6 | describe('middleware: ', () => {
7 | test("actions' middlewareConfig", async () => {
8 | let state: any
9 | let actions: any
10 | const { useStore } = Model({ Counter })
11 | renderHook(() => {
12 | ;[state, actions] = useStore('Counter')
13 | })
14 | await actions.add(3, { params: 'user-custom-params' })
15 | expect(state).toEqual({ count: 3 })
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/__test__/middlewares/model.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | process.env.NODE_ENV = 'production'
3 | import { renderHook } from '@testing-library/react-hooks'
4 | import { NextCounterModel } from '..'
5 | import { Model } from '../../src'
6 |
7 | describe('NextModel', () => {
8 | test("allows you to customize model's middleware", async () => {
9 | let actions: any, nextActions: any
10 | let state: any, nextState: any
11 | const WrapperModel = Model(NextCounterModel)
12 | const { useStore, getActions } = Model({ NextCounterModel, WrapperModel })
13 | const beginTime = Date.now()
14 | renderHook(() => {
15 | ;[state, actions] = useStore('NextCounterModel')
16 | ;[nextState, nextActions] = useStore('WrapperModel')
17 | })
18 | await actions.increment(2)
19 | await getActions('NextCounterModel').increment(1)
20 | expect(Date.now() - beginTime > 300)
21 | expect(state.count).toBe(3)
22 | await nextActions.increment(2)
23 | await getActions('WrapperModel').increment(1)
24 | expect(nextState.count).toBe(3)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/__test__/middlewares/subscribe.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Model } from '../../src'
3 | import { NextCounter } from '..'
4 | import { renderHook } from '@testing-library/react-hooks'
5 |
6 | describe('Subscribe middleware', () => {
7 | test('run callback when specific action run', async () => {
8 | let actions: any
9 | let count = 0
10 | let incrementCount = 0
11 | const Counter = Model(NextCounter)
12 | const { useStore, subscribe } = Model({ Counter })
13 | subscribe('Counter', ['increment'], () => (count += 1))
14 | subscribe('Counter', 'add', () => (count += 10))
15 | subscribe('Counter', ['increment', 'add'], () => (count += 5))
16 | subscribe('Counter', 'increment', ({ params }) => {
17 | incrementCount += params || 1
18 | })
19 | renderHook(() => {
20 | ;[, actions] = useStore('Counter')
21 | })
22 | await actions.increment()
23 | await actions.add(1)
24 | await actions.increment()
25 | await actions.increment()
26 | expect(count).toBe(33)
27 | await actions.addCaller()
28 | expect(count).toBe(48)
29 | expect(incrementCount).toBe(3)
30 | await actions.increment(10)
31 | expect(incrementCount).toBe(13)
32 | await actions.increment(-3)
33 | expect(incrementCount).toBe(10)
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/__test__/middlewares/tryCatch.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | process.env.NODE_ENV = 'production'
3 | import { Model, middlewares } from '../../src'
4 | import { ErrorCounter } from '..'
5 | import { renderHook } from '@testing-library/react-hooks'
6 |
7 | describe('tryCatch', () => {
8 | test("catch actions' error in production", async () => {
9 | let actions: any
10 | let errNum = 0
11 | const { useStore } = Model({ ErrorCounter })
12 | renderHook(() => {
13 | ;[, actions] = useStore('ErrorCounter')
14 | })
15 | await actions.increment().catch(() => {
16 | errNum += 1
17 | })
18 | expect(errNum).toBe(0)
19 | })
20 |
21 | test("throw actions' error when turn off tryCatch middleware", async () => {
22 | let actions: any
23 | let errNum = 0
24 | middlewares.config.tryCatch.enable = false
25 | const { useStore } = Model({ ErrorCounter })
26 | renderHook(() => {
27 | ;[, actions] = useStore('ErrorCounter')
28 | })
29 | await actions.increment().catch(() => {
30 | errNum += 1
31 | })
32 | expect(errNum).toBe(1)
33 | })
34 | })
35 |
--------------------------------------------------------------------------------
/__test__/middlewares/unsubscribe.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { Model } from '../../src'
3 | import { Counter } from '..'
4 | import { renderHook } from '@testing-library/react-hooks'
5 |
6 | describe('Subscribe middleware', () => {
7 | test('run callback when specific action run', async () => {
8 | let actions: any
9 | let count = 0
10 | const { useStore, subscribe, unsubscribe } = Model({ Counter })
11 | subscribe('Counter', ['increment'], () => (count += 1))
12 | subscribe('Counter', 'add', () => (count += 10))
13 | subscribe('Counter', ['increment', 'add'], () => (count += 5))
14 | renderHook(() => {
15 | ;[, actions] = useStore('Counter')
16 | })
17 | await actions.increment()
18 | unsubscribe('Counter', 'add')
19 | await actions.add(1)
20 | unsubscribe('Counter', ['add', 'increment'])
21 | await actions.increment()
22 | await actions.increment()
23 | expect(count).toBe(6)
24 | })
25 | })
26 |
--------------------------------------------------------------------------------
/__test__/performance/deep-expensive-mutation.spec.tsx:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook, act } from '@testing-library/react-hooks'
3 | import { Model } from '../../src'
4 | import { ExpensiveModel } from '..'
5 |
6 | describe('setState', () => {
7 | test("circular state is forbidden by default", async () => {
8 | // @ts-ignore
9 | let isCounterOdd: any,
10 | // @ts-ignore
11 | state: any
12 | let actions: any
13 | let renderTime = 0
14 | const { useStore } = Model(ExpensiveModel)
15 | renderHook(() => {
16 | ;[state, actions] = useStore()
17 | renderTime += 1
18 | })
19 | renderHook(() => {
20 | ;[state] = useStore()
21 | })
22 | // throw error when set circular state without freezing
23 | // rerender is not invoked
24 | await act(async () => {
25 | try {
26 | await actions.setState()
27 | } catch (e) {
28 | expect(e.message).toContain("Maximum call stack size exceeded")
29 | }
30 | })
31 | expect(renderTime).toBe(1)
32 | })
33 |
34 | test("pre freeze circular state is allowed", async () => {
35 | // @ts-ignore
36 | let isCounterOdd: any,
37 | // @ts-ignore
38 | state: any
39 | let actions: any
40 | let renderTime = 0
41 | const { useStore } = Model(ExpensiveModel)
42 | renderHook(() => {
43 | ;[state, actions] = useStore()
44 | renderTime += 1
45 | })
46 | renderHook(() => {
47 | ;[state] = useStore()
48 | })
49 | // setState passed when circular state has been freezed
50 | // rerender is invoked
51 | await act(async () => {
52 | try {
53 | await actions.setPreFreezedDataset()
54 | } catch (e) {
55 | expect(true).toBe(false)
56 | }
57 | })
58 | expect(renderTime).toBe(2)
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/__test__/selector/model.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Model } from '../../src'
4 | import { Counter } from '..'
5 |
6 | describe('selector', () => {
7 | test("model's selector", async () => {
8 | let isCounterOdd: boolean = true,
9 | state: any
10 | let actionsFirst: any, actionsSecond: any
11 | let renderTime = 0
12 | const { useStore, actions } = Model(Counter)
13 | renderHook(() => {
14 | ;[isCounterOdd, actionsFirst] = useStore(({ count }) => count % 2 !== 0)
15 | renderTime += 1
16 | })
17 | renderHook(() => {
18 | ;[state, actionsSecond] = useStore()
19 | })
20 | expect(isCounterOdd).toBe(false)
21 | expect(state.count).toBe(0)
22 | expect(renderTime).toBe(1)
23 | await actionsFirst.increment(3)
24 | expect(isCounterOdd).toBe(true)
25 | expect(state.count).toBe(3)
26 | expect(renderTime).toBe(2)
27 | await actionsSecond.increment(4)
28 | expect(isCounterOdd).toBe(true)
29 | expect(state.count).toBe(7)
30 | expect(renderTime).toBe(2)
31 | await actions.increment(4)
32 | expect(isCounterOdd).toBe(true)
33 | expect(state.count).toBe(11)
34 | expect(renderTime).toBe(2)
35 | await actions.add(1)
36 | expect(isCounterOdd).toBe(false)
37 | expect(state.count).toBe(12)
38 | expect(renderTime).toBe(3)
39 | })
40 | })
41 |
--------------------------------------------------------------------------------
/__test__/selector/models.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Model } from '../../src'
4 | import { Counter } from '..'
5 |
6 | describe('selector', () => {
7 | test("models' selector", async () => {
8 | let isCounterOdd: boolean = true,
9 | state: any
10 | let actionsFirst: any, actionsSecond: any
11 | let renderTime = 0
12 | const { useStore, actions } = Model({ Counter })
13 | renderHook(() => {
14 | ;[isCounterOdd, actionsFirst] = useStore(
15 | 'Counter',
16 | ({ count }) => count % 2 !== 0
17 | )
18 | renderTime += 1
19 | })
20 | renderHook(() => {
21 | ;[state, actionsSecond] = useStore('Counter')
22 | })
23 | expect(isCounterOdd).toBe(false)
24 | expect(state.count).toBe(0)
25 | expect(renderTime).toBe(1)
26 | await actionsFirst.increment(3)
27 | expect(isCounterOdd).toBe(true)
28 | expect(state.count).toBe(3)
29 | expect(renderTime).toBe(2)
30 | await actionsSecond.increment(4)
31 | expect(isCounterOdd).toBe(true)
32 | expect(state.count).toBe(7)
33 | expect(renderTime).toBe(2)
34 | await actions.Counter.increment(4)
35 | expect(isCounterOdd).toBe(true)
36 | expect(state.count).toBe(11)
37 | expect(renderTime).toBe(2)
38 | await actions.Counter.add(1)
39 | expect(isCounterOdd).toBe(false)
40 | expect(state.count).toBe(12)
41 | expect(renderTime).toBe(3)
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/__test__/selector/shallowEqual.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Model } from '../../src'
4 | import { Counter } from '..'
5 |
6 | describe('selector', () => {
7 | test("model's selector", async () => {
8 | let selectedState: { odd: boolean; count?: number } = { odd: true },
9 | state: any
10 | let actionsFirst: any, actionsSecond: any
11 | let renderTime = 0
12 | const { useStore, actions } = Model(Counter)
13 | renderHook(() => {
14 | ;[selectedState, actionsFirst] = useStore(({ count }) =>
15 | count < 10
16 | ? {
17 | odd: count % 2 !== 0
18 | }
19 | : {
20 | count,
21 | odd: count % 2 !== 0
22 | }
23 | )
24 | renderTime += 1
25 | })
26 | renderHook(() => {
27 | ;[state, actionsSecond] = useStore()
28 | })
29 | expect(selectedState.odd).toBe(false)
30 | expect(state.count).toBe(0)
31 | expect(renderTime).toBe(1)
32 | await actionsFirst.increment(3)
33 | // odd state change, rerender
34 | expect(selectedState.odd).toBe(true)
35 | expect(state.count).toBe(3)
36 | expect(renderTime).toBe(2)
37 | await actionsSecond.increment(4)
38 | expect(selectedState.odd).toBe(true)
39 | expect(state.count).toBe(7)
40 | expect(renderTime).toBe(2)
41 | await actions.increment(4)
42 | // selected keys num + 1, rerender
43 | expect(selectedState.odd).toBe(true)
44 | expect(state.count).toBe(11)
45 | expect(renderTime).toBe(3)
46 | await actions.add(1)
47 | // odd state change, rerender
48 | expect(selectedState.odd).toBe(false)
49 | expect(state.count).toBe(12)
50 | expect(renderTime).toBe(4)
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/__test__/useStore/actions.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Counter } from '..'
4 | import { Model } from '../../src'
5 |
6 | describe('useStore', () => {
7 | test('consumer actions return Partial', async () => {
8 | let state: any
9 | let actions: any
10 | const { useStore } = Model({ Counter })
11 | renderHook(() => {
12 | ;[state, actions] = useStore('Counter')
13 | })
14 | await actions.add(3)
15 | expect(state).toEqual({ count: 3 })
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/__test__/useStore/initial.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { renderHook } from '@testing-library/react-hooks'
3 | import { Model } from '../../src'
4 | import { Counter } from '..'
5 |
6 | describe('useStore', () => {
7 | test('return default initial values', () => {
8 | let state
9 | const { useStore } = Model({ Counter })
10 | renderHook(() => {
11 | ;[state] = useStore('Counter')
12 | })
13 | expect(state).toEqual({ count: 0 })
14 | })
15 | test('consumer actions return function', async () => {
16 | let state: any
17 | let actions: any
18 | const { useStore } = Model({ Counter })
19 | renderHook(() => {
20 | ;[state, actions] = useStore('Counter')
21 | })
22 | await actions.increment(3)
23 | expect(state).toEqual({ count: 3 })
24 | await actions.increment(4)
25 | expect(state.count).toBe(7)
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/__test__/useStore/initialModels.spec.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { AsyncCounter } from '..'
3 | import { Model } from '../../src'
4 |
5 | describe('useStore', () => {
6 | test('use initialModels', async () => {
7 | const { getInitialState } = Model({ AsyncCounter })
8 | const initialModels = await getInitialState()
9 | const { getState } = Model({ AsyncCounter }, initialModels)
10 | const state = getState('AsyncCounter')
11 | expect(state.count).toBe(1)
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {extends: ['@commitlint/config-conventional']}
2 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // For a detailed explanation regarding each configuration property, visit:
2 | // https://jestjs.io/docs/en/configuration.html
3 |
4 | module.exports = {
5 | // All imported modules in your tests should be mocked automatically
6 | // automock: false,
7 |
8 | // Stop running tests after `n` failures
9 | // bail: 0,
10 |
11 | // Respect "browser" field in package.json when resolving modules
12 | // browser: false,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/h9/lgs7v_813jl7rdb_bfsqdw700000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls and instances between every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | // collectCoverage: false,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: null,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: 'coverage',
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | coveragePathIgnorePatterns: ['/node_modules/', '/__test__/'],
31 |
32 | // A list of reporter names that Jest uses when writing coverage reports
33 | coverageReporters: ['json-summary', 'json', 'text', 'lcov', 'clover'],
34 |
35 | // An object that configures minimum threshold enforcement for coverage results
36 | // coverageThreshold: null,
37 |
38 | // A path to a custom dependency extractor
39 | // dependencyExtractor: null,
40 |
41 | // Make calling deprecated APIs throw helpful error messages
42 | // errorOnDeprecated: false,
43 |
44 | // Force coverage collection from ignored files usin a array of glob patterns
45 | // forceCoverageMatch: [],
46 |
47 | // A path to a module which exports an async function that is triggered once before all test suites
48 | // globalSetup: null,
49 |
50 | // A path to a module which exports an async function that is triggered once after all test suites
51 | // globalTeardown: null,
52 |
53 | // A set of global variables that need to be available in all test environments
54 | // globals: {},
55 |
56 | // An array of directory names to be searched recursively up from the requiring module's location
57 | // moduleDirectories: [
58 | // "node_modules"
59 | // ],
60 |
61 | // An array of file extensions your modules use
62 | // moduleFileExtensions: [
63 | // "js",
64 | // "json",
65 | // "jsx",
66 | // "ts",
67 | // "tsx",
68 | // "node"
69 | // ],
70 |
71 | // A map from regular expressions to module names that allow to stub out resources with a single module
72 | // moduleNameMapper: {},
73 |
74 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
75 | // modulePathIgnorePatterns: [],
76 |
77 | // Activates notifications for test results
78 | // notify: false,
79 |
80 | // An enum that specifies notification mode. Requires { notify: true }
81 | // notifyMode: "failure-change",
82 |
83 | // A preset that is used as a base for Jest's configuration
84 | preset: 'ts-jest'
85 |
86 | // Run tests from one or more projects
87 | // projects: null,
88 |
89 | // Use this configuration option to add custom reporters to Jest
90 | // reporters: undefined,
91 |
92 | // Automatically reset mock state between every test
93 | // resetMocks: false,
94 |
95 | // Reset the module registry before running each individual test
96 | // resetModules: false,
97 |
98 | // A path to a custom resolver
99 | // resolver: null,
100 |
101 | // Automatically restore mock state between every test
102 | // restoreMocks: false,
103 |
104 | // The root directory that Jest should scan for tests and modules within
105 | // rootDir: null,
106 |
107 | // A list of paths to directories that Jest should use to search for files in
108 | // roots: [
109 | // ""
110 | // ],
111 |
112 | // Allows you to use a custom runner instead of Jest's default test runner
113 | // runner: "jest-runner",
114 |
115 | // The paths to modules that run some code to configure or set up the testing environment before each test
116 | // setupFiles: [],
117 |
118 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
119 | // setupFilesAfterEnv: [],
120 |
121 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
122 | // snapshotSerializers: [],
123 |
124 | // The test environment that will be used for testing
125 | // testEnvironment: "jest-environment-jsdom",
126 |
127 | // Options that will be passed to the testEnvironment
128 | // testEnvironmentOptions: {},
129 |
130 | // Adds a location field to test results
131 | // testLocationInResults: false,
132 |
133 | // The glob patterns Jest uses to detect test files
134 | // ,testMatch: [
135 | // "**/__test__/**/react.spec.[jt]s?(x)",
136 | // // "**/?(*.)+(spec|test).[tj]s?(x)"
137 | // ],
138 |
139 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
140 | // testPathIgnorePatterns: [
141 | // "/node_modules/"
142 | // ],
143 |
144 | // The regexp pattern or array of patterns that Jest uses to detect test files
145 | // testRegex: [],
146 |
147 | // This option allows the use of a custom results processor
148 | // testResultsProcessor: null,
149 |
150 | // This option allows use of a custom test runner
151 | // testRunner: "jasmine2",
152 |
153 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
154 | // testURL: "http://localhost",
155 |
156 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
157 | // timers: "real",
158 |
159 | // A map from regular expressions to paths to transformers
160 | // transform: null,
161 |
162 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
163 | // transformIgnorePatterns: [
164 | // "/node_modules/"
165 | // ],
166 |
167 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
168 | // unmockedModulePathPatterns: undefined,
169 |
170 | // Indicates whether each individual test should be reported during the run
171 | // verbose: null,
172 |
173 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
174 | // watchPathIgnorePatterns: [],
175 |
176 | // Whether to use watchman for file crawling
177 | // watchman: true,
178 | }
179 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-model",
3 | "version": "4.3.0",
4 | "description": "The State management library for React",
5 | "main": "./dist/react-model.js",
6 | "module": "./dist/react-model.esm.js",
7 | "umd:main": "./dist/react-model.umd.js",
8 | "types": "./src/index",
9 | "scripts": {
10 | "build:prod": "microbundle --define process.env.NODE_ENV=production --sourcemap false --jsx React.createElement --output dist --tsconfig ./tsconfig.json",
11 | "build:dev": "microbundle --define process.env.NODE_ENV=development --sourcemap true --jsx React.createElement --output dist --tsconfig ./tsconfig.json",
12 | "commit": "git-cz",
13 | "lint-ts": "tslint -c tslint.json 'src/**/*.ts'",
14 | "lint-md": "remark .",
15 | "test": "jest --silent",
16 | "test:coverage": "jest --collect-coverage --silent"
17 | },
18 | "keywords": ["react", "model", "state-management", "react-hooks"],
19 | "author": "ArrayZoneYour ",
20 | "license": "MIT",
21 | "dependencies": {
22 | "immer": ">=8.0.1 <10.0.0"
23 | },
24 | "peerDependencies": {
25 | "react": ">=16.3.0",
26 | "react-dom": ">=16.3.0",
27 | "typescript": ">=3.9.0"
28 | },
29 | "devDependencies": {
30 | "@commitlint/cli": "^16.3.0",
31 | "@commitlint/config-conventional": "^16.2.4",
32 | "@testing-library/react": "^10.0.1",
33 | "@testing-library/react-hooks": "^2.0.1",
34 | "@types/babel__core": "^7.1.1",
35 | "@types/babel__template": "^7.0.2",
36 | "@types/faker": "^5.5.3",
37 | "@types/jest": "^25.1.0",
38 | "@types/node": "^14.0.0",
39 | "@types/react": "^16.9.1",
40 | "@types/react-dom": "^16.8.0",
41 | "commitizen": "^4.0.0",
42 | "cz-conventional-changelog": "^3.0.0",
43 | "faker": "^5.5.3",
44 | "husky": "^4.0.2",
45 | "jest": "^24.1.0",
46 | "microbundle": "^0.12.3",
47 | "prettier": "^2.0.0",
48 | "react": "^16.8.4",
49 | "react-dom": "^16.8.4",
50 | "react-test-renderer": "^16.8.6",
51 | "remark-cli": "^10.0.1",
52 | "remark-lint": "^9.1.1",
53 | "remark-preset-lint-recommended": "^6.1.2",
54 | "ts-jest": "^26.0.0",
55 | "tslint": "^5.14.0",
56 | "typescript": "^3.4.5"
57 | },
58 | "husky": {
59 | "hooks": {
60 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
61 | }
62 | },
63 | "repository": {
64 | "type": "git",
65 | "url": "git+https://github.com/byte-fe/react-model"
66 | },
67 | "resolutions": {
68 | "minimist": ">=1.2.6"
69 | },
70 | "bugs": {
71 | "url": "https://github.com/byte-fe/react-model/issues"
72 | },
73 | "homepage": "https://github.com/byte-fe/react-model#readme",
74 | "config": {
75 | "commitizen": {
76 | "path": "./node_modules/cz-conventional-changelog"
77 | }
78 | },
79 | "browserslist": [
80 | "edge 17",
81 | "firefox 70",
82 | "chrome 48",
83 | "safari 12.1",
84 | "android 4.0",
85 | "samsung 9.2"
86 | ]
87 | }
88 |
--------------------------------------------------------------------------------
/src/global.ts:
--------------------------------------------------------------------------------
1 | const State = {}
2 | const mutableState = {}
3 | const Actions = {}
4 | const AsyncState = {}
5 | const Middlewares = {}
6 | // Communicate between Provider-Consumer and Hooks
7 | const Setter: Setter = {
8 | // classSetter stores the setState from Provider
9 | // Invoke the classSetter.setState can update the state of Global Provider.
10 | classSetter: undefined,
11 | // functionSetter stores the setState returned by useStore.
12 | // These setStates can invoke the rerender of hooks components.
13 | functionSetter: {}
14 | }
15 |
16 | const Context = {
17 | __global: {}
18 | }
19 |
20 | const subscriptions = {}
21 |
22 | let devTools: any
23 | let withDevTools = false
24 |
25 | let uid = 0 // The unique id of hooks
26 | let storeId = 0 // The unique id of stores
27 | let currentStoreId = '0' // Used for useModel
28 | let gid = 0 // The unique id of models' group
29 |
30 | export default {
31 | Actions,
32 | AsyncState,
33 | Context,
34 | Middlewares,
35 | Setter,
36 | State,
37 | devTools,
38 | subscriptions,
39 | mutableState,
40 | gid,
41 | uid,
42 | storeId,
43 | currentStoreId,
44 | withDevTools
45 | } as Global
46 |
--------------------------------------------------------------------------------
/src/helper.ts:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 | import { createContext } from 'react'
3 | import Global from './global'
4 | import { actionMiddlewares, applyMiddlewares } from './middlewares'
5 |
6 | const initialProviderState: Global['State'] = {}
7 | const GlobalContext = createContext(initialProviderState)
8 | const Consumer = GlobalContext.Consumer
9 |
10 | // console.group polyfill
11 | if (!console.group) {
12 | const groups: any[] = []
13 | const hr = '-'.repeat(80) // 80 dashes row line
14 | console.group = function logGroupStart(label: any) {
15 | groups.push(label)
16 | console.log('%c \nBEGIN GROUP: %c', hr, label)
17 | console.groupEnd = function logGroupEnd() {
18 | console.log('END GROUP: %c\n%c', groups.pop(), hr)
19 | }
20 | }
21 | }
22 |
23 | const consumerAction = (
24 | action: Action,
25 | modelContext: { modelName: string }
26 | ) => async (params: any, middlewareConfig?: any) => {
27 | const context: InnerContext = {
28 | Global,
29 | action,
30 | actionName: action.name,
31 | consumerActions,
32 | middlewareConfig,
33 | modelName: modelContext.modelName,
34 | newState: null,
35 | params,
36 | type: 'o'
37 | }
38 | return await applyMiddlewares(actionMiddlewares, context)
39 | }
40 |
41 | const consumerActions = (
42 | actions: Actions,
43 | modelContext: { modelName: string }
44 | ) => {
45 | const ret: any = {}
46 | Object.keys(actions).forEach((key) => {
47 | // @ts-ignore
48 | ret[key] = consumerAction(actions[key], modelContext)
49 | })
50 | return ret
51 | }
52 |
53 | const setPartialState = (
54 | name: keyof typeof Global['State'],
55 | partialState:
56 | | typeof Global['State']
57 | | ((state: typeof Global['State']['name']) => void)
58 | ) => {
59 | if (typeof partialState === 'function') {
60 | let state = Global.State[name]
61 | state = produce(state, partialState)
62 | Global.State = produce(Global.State, (s) => {
63 | s[name] = state
64 | })
65 | } else {
66 | Global.State = produce(Global.State, (s) => {
67 | s[name] = {
68 | ...s[name],
69 | ...partialState
70 | }
71 | })
72 | }
73 | return Global.State
74 | }
75 |
76 | const get = (p: Array) => (o: T) =>
77 | p.reduce((xs: any, key) => (xs && xs[key] ? xs[key] : null), o)
78 |
79 | const timeout = (ms: number, data: T): Promise =>
80 | new Promise((resolve) =>
81 | setTimeout(() => {
82 | console.log(ms)
83 | resolve(data)
84 | }, ms)
85 | )
86 |
87 | const getInitialState = async (
88 | context?: T,
89 | config?: { isServer?: boolean; prefix?: string }
90 | ) => {
91 | const ServerState: { [name: string]: any } = { __FROM_SERVER__: true }
92 | await Promise.all(
93 | Object.keys(Global.State).map(async (modelName) => {
94 | let prefix = (config && config.prefix) || ''
95 | if (
96 | !context ||
97 | !context.modelName ||
98 | modelName === prefix + context.modelName ||
99 | context.modelName.indexOf(prefix + modelName) !== -1
100 | ) {
101 | const asyncGetter = Global.AsyncState[modelName]
102 | const asyncState = asyncGetter ? await asyncGetter(context) : {}
103 | if (config && config.isServer) {
104 | ServerState[modelName] = asyncState
105 | } else {
106 | Global.State = produce(Global.State, (s) => {
107 | s[modelName] = { ...s[modelName], ...asyncState }
108 | })
109 | }
110 | }
111 | })
112 | )
113 | return config && config.isServer ? ServerState : Global.State
114 | }
115 |
116 | const getCache = (modelName: string, actionName: string) => {
117 | const JSONString = localStorage.getItem(
118 | `__REACT_MODELX__${modelName}_${actionName}`
119 | )
120 | return JSONString ? JSON.parse(JSONString) : null
121 | }
122 |
123 | const shallowEqual = (objA: any, objB: any) => {
124 | if (objA === objB) return true
125 | if (
126 | typeof objA !== 'object' ||
127 | objA === null ||
128 | typeof objB !== 'object' ||
129 | objB === null
130 | ) {
131 | return false
132 | }
133 | const keysA = Object.keys(objA)
134 | const keysB = Object.keys(objB)
135 |
136 | if (keysA.length !== keysB.length) return false
137 |
138 | for (let i = 0; i < keysA.length; i++) {
139 | if (
140 | !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
141 | objA[keysA[i]] !== objB[keysA[i]]
142 | ) {
143 | return false
144 | }
145 | }
146 |
147 | return true
148 | }
149 |
150 | export {
151 | Consumer,
152 | consumerActions,
153 | GlobalContext,
154 | setPartialState,
155 | shallowEqual,
156 | timeout,
157 | get,
158 | getCache,
159 | getInitialState
160 | }
161 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | type Setter = {
2 | classSetter: ClassSetter
3 | functionSetter: FunctionSetter
4 | }
5 |
6 | type FunctionSetter = {
7 | [modelName: string]: {
8 | [actionName: string]: {
9 | setState: React.Dispatch
10 | selector?: Function
11 | selectorRef?: unknown
12 | }
13 | }
14 | }
15 |
16 | type Equals = (() => T extends X ? 1 : 2) extends () => T extends Y
17 | ? 1
18 | : 2
19 | ? true
20 | : false
21 |
22 | interface Global {
23 | Actions: {
24 | [modelName: string]: {
25 | [actionName: string]: Action
26 | }
27 | }
28 | State: {
29 | [modelName: string]: any
30 | }
31 | mutableState: {
32 | [modelName: string]: any
33 | }
34 | AsyncState: {
35 | [modelName: string]: undefined | ((context?: any) => Promise>)
36 | }
37 | Context: any
38 | Middlewares: {
39 | [modelName: string]: Middleware[]
40 | }
41 | subscriptions: Subscriptions
42 | Setter: Setter
43 | devTools: any
44 | withDevTools: boolean
45 | gid: number
46 | uid: number
47 | storeId: number
48 | currentStoreId: string
49 | }
50 |
51 | type ClassSetter = React.Dispatch | undefined
52 |
53 | type Action = (
54 | params: P,
55 | context: {
56 | state: S
57 | actions: getConsumerActionsType>
58 | } & ExtContext
59 | ) =>
60 | | Partial
61 | | Promise | ProduceFunc | void>
62 | | ProduceFunc
63 | | void
64 |
65 | type ProduceFunc = (state: S) => void
66 |
67 | // v3.0 Actions
68 | type Actions = {
69 | [P in keyof ActionKeys]: Action
70 | }
71 |
72 | // v4.1+ Custom Hooks
73 | type CustomModelHook = () => State
74 |
75 | type Dispatch = (value: A) => void
76 | type SetStateAction = S | ((prevState: S) => S)
77 |
78 | interface ModelContext {
79 | modelName: string
80 | }
81 |
82 | interface BaseContext {
83 | action: Action
84 | consumerActions: (
85 | actions: Actions,
86 | modelContext: ModelContext
87 | ) => getConsumerActionsType
88 | params: P
89 | middlewareConfig?: Object
90 | actionName: string
91 | modelName: string
92 | next?: Function
93 | disableSelectorUpdate?: boolean
94 | newState: Global['State'] | Function | null
95 | Global: Global
96 | }
97 |
98 | interface InnerContext extends BaseContext {
99 | // Actions with function type context will always invoke current component's reload.
100 | // f -> function, o -> outer, c -> class, u -> useModel
101 | type?: 'f' | 'o' | 'c' | 'u'
102 | __hash?: string
103 | }
104 |
105 | type Context = InnerContext & {
106 | next: Function
107 | modelMiddlewares?: Middleware[]
108 | }
109 |
110 | type Middleware = (C: Context, M: Middleware[]) => Promise
111 |
112 | type MiddlewareConfig = {
113 | logger: {
114 | enable: boolean | ((context: BaseContext) => boolean)
115 | }
116 | devtools: { enable: boolean }
117 | tryCatch: { enable: boolean }
118 | }
119 |
120 | interface Models {
121 | [name: string]:
122 | | ModelType
123 | | API>
124 | }
125 |
126 | type Selector = (state: S) => R
127 |
128 | interface API> {
129 | __id: string
130 | __ERROR__?: boolean
131 | useStore: <
132 | F extends Selector, any> = Selector<
133 | Get,
134 | unknown
135 | >
136 | >(
137 | selector?: F
138 | ) => [
139 | F extends Selector, any>
140 | ? Equals, unknown>> extends true
141 | ? Get
142 | : ReturnType
143 | : Get,
144 | getConsumerActionsType>
145 | ]
146 | getState: () => Readonly>
147 | subscribe: (
148 | actionName: keyof MT['actions'] | Array,
149 | callback: (context: BaseContext) => void
150 | ) => void
151 | unsubscribe: (
152 | actionName: keyof Get | Array>
153 | ) => void
154 | actions: Readonly>>
155 | }
156 |
157 | interface LaneAPI {
158 | useStore: () => S
159 | getState: () => S
160 | getStore: () => S | undefined
161 | subscribe: (callback: () => void) => void
162 | unsubscribe: (callback: () => void) => void
163 | }
164 |
165 | interface APIs {
166 | useStore: <
167 | K extends keyof M,
168 | S extends M[K] extends API
169 | ? ArgumentTypes>[1]
170 | : M[K] extends ModelType
171 | ? Selector, unknown>
172 | : any
173 | >(
174 | name: K,
175 | selector?: S
176 | ) => M[K] extends API
177 | ? S extends (...args: any) => void
178 | ? Equals extends true
179 | ? [ReturnType>, Get]
180 | : ReturnType
181 | : ReturnType>
182 | : M[K] extends ModelType
183 | ? S extends (...args: any) => void
184 | ? [
185 | Equals, unknown> extends true
186 | ? Get
187 | : ReturnType,
188 | getConsumerActionsType>
189 | ]
190 | : [Get, getConsumerActionsType>]
191 | : any
192 |
193 | getState: (
194 | modelName: K
195 | ) => M[K] extends ModelType
196 | ? Readonly>
197 | : M[K] extends API
198 | ? ReturnType>
199 | : any
200 | getActions: (
201 | modelName: K
202 | ) => M[K] extends ModelType
203 | ? Readonly>>
204 | : M[K] extends API
205 | ? M[K]['actions']
206 | : unknown
207 | getInitialState: (
208 | context?: T | undefined,
209 | config?: { isServer: boolean }
210 | ) => Promise<{
211 | [modelName: string]: any
212 | }>
213 | subscribe: (
214 | modelName: K,
215 | actionName: keyof Get | Array>,
216 | callback: (context: BaseContext) => void
217 | ) => void
218 | unsubscribe: (
219 | modelName: K,
220 | actionName: keyof Get | Array>
221 | ) => void
222 | actions: {
223 | [K in keyof M]: M[K] extends API
224 | ? M[K]['actions']
225 | : Readonly>>
226 | }
227 | }
228 |
229 | // v3.0
230 | type ModelType<
231 | InitStateType = any,
232 | ActionKeys = any,
233 | ExtContext extends {} = {}
234 | > = {
235 | __ERROR__?: boolean
236 | actions: {
237 | [P in keyof ActionKeys]: Action<
238 | InitStateType,
239 | ActionKeys[P],
240 | ActionKeys,
241 | ExtContext
242 | >
243 | }
244 | middlewares?: Middleware[]
245 | state: InitStateType
246 | asyncState?: (context?: any) => Promise>
247 | }
248 |
249 | type ArgumentTypes = F extends (...args: infer A) => any
250 | ? A
251 | : never
252 |
253 | // v3.0
254 | // TODO: ArgumentTypes[0] = undefined | string
255 | type getConsumerActionsType> = {
256 | [P in keyof A]: ArgumentTypes[0] extends undefined
257 | ? (params?: ArgumentTypes[0]) => ReturnType
258 | : (params: ArgumentTypes[0]) => ReturnType
259 | }
260 |
261 | type Get