├── .codecov.yml
├── .github
└── ISSUE_TEMPLATE
│ ├── custom.md
│ └── issue-template.md
├── .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
│ └── 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
├── 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
├── 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
/.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 |
--------------------------------------------------------------------------------
/.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 | yarn.lock
24 | package-lock.json
25 |
26 | # Jest
27 | coverage/
28 |
--------------------------------------------------------------------------------
/.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://travis-ci.org/byte-fe/react-model) [](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 { Model } from 'react-model'
21 |
22 | // define model
23 | const Todo = {
24 | state: {
25 | items: ['Install react-model', 'Read github docs', 'Build App']
26 | },
27 | actions: {
28 | add: todo => {
29 | // s is the readonly version of state
30 | // you can also return partial state here but don't need to keep immutable manually
31 | // state is the mutable state
32 | return state => {
33 | state.items.push(todo)
34 | }
35 | }
36 | }
37 | }
38 |
39 | // Model Register
40 | const { useStore } = Model(Todo)
41 |
42 | const App = () => {
43 | return
44 | }
45 |
46 | const TodoList = () => {
47 | const [state, actions] = useStore()
48 | return
49 |
50 | {state.items.map((item, index) => (
))}
51 |
52 | }
53 | ```
54 |
55 | ---
56 |
57 | ## Recently Updated
58 |
59 | * [feat(middleware): support enable/disable sepecific middleware](#how-can-i-disable-the-console-debugger)
60 | * fix(stateupdater): fix the issue that setState on unmounted component
61 |
62 | ## Quick Start
63 |
64 | [CodeSandbox: TodoMVC](https://codesandbox.io/s/moyxon99jx)
65 |
66 | [Next.js + react-model work around](https://github.com/byte-fe/react-model-experiment)
67 |
68 | [v2 docs](https://github.com/byte-fe/react-model/blob/v2/README.md)
69 |
70 | install package
71 |
72 | ```shell
73 | npm install react-model
74 | ```
75 |
76 | ## Table of Contents
77 |
78 | - [Core Concept](#core-concept)
79 | - [Model](#model)
80 | - [Model Register](#model-register)
81 | - [useStore](#usestore)
82 | - [getState](#getstate)
83 | - [actions](#actions)
84 | - [subscribe](#subscribe)
85 | - [Advance Concept](#advance-concept)
86 | - [immutable Actions](#immutable-actions)
87 | - [SSR with Next.js](#ssr-with-nextjs)
88 | - [Middleware](#middleware)
89 | - [Expand Context](#expand-context)
90 | - [Other Concept required by Class Component](#other-concept-required-by-class-component)
91 | - [Provider](#provider)
92 | - [connect](#connect)
93 | - [FAQ](#faq)
94 | - [Migrate from 3.1.x to 4.x.x](#migrate-from-31x-to-4xx)
95 | - [How can I disable the console debugger?](#how-can-i-disable-the-console-debugger)
96 | - [How can I add custom middleware](#how-can-i-add-custom-middleware)
97 | - [How can I make persist models](#how-can-i-make-persist-models)
98 | - [How can I deal with local state](#how-can-i-deal-with-local-state)
99 | - [actions throw error from immer.module.js](#actions-throw-error-from-immermodulejs)
100 | - [How can I customize each model's middlewares?](#how-can-i-customize-each-models-middlewares)
101 |
102 | ## Core Concept
103 |
104 | ### Model
105 |
106 | Every model has its own state and actions.
107 |
108 | ```typescript
109 | const initialState = {
110 | counter: 0,
111 | light: false,
112 | response: {}
113 | }
114 |
115 | interface StateType {
116 | counter: number
117 | light: boolean
118 | response: {
119 | code?: number
120 | message?: string
121 | }
122 | }
123 |
124 | interface ActionsParamType {
125 | increment: number
126 | openLight: undefined
127 | get: undefined
128 | } // You only need to tag the type of params here !
129 |
130 | const model: ModelType = {
131 | actions: {
132 | increment: async (payload, { state }) => {
133 | return {
134 | counter: state.counter + (payload || 1)
135 | }
136 | },
137 | openLight: async (_, { state, actions }) => {
138 | await actions.increment(1) // You can use other actions within the model
139 | await actions.get() // support async functions (block actions)
140 | actions.get()
141 | await actions.increment(1) // + 1
142 | await actions.increment(1) // + 2
143 | await actions.increment(1) // + 3 as expected !
144 | return { light: !state.light }
145 | },
146 | get: async () => {
147 | await new Promise((resolve, reject) =>
148 | setTimeout(() => {
149 | resolve()
150 | }, 3000)
151 | )
152 | return {
153 | response: {
154 | code: 200,
155 | message: `${new Date().toLocaleString()} open light success`
156 | }
157 | }
158 | }
159 | },
160 | state: initialState
161 | }
162 |
163 | export default model
164 |
165 | // You can use these types when use Class Components.
166 | // type ConsumerActionsType = getConsumerActionsType
167 | // type ConsumerType = { actions: ConsumerActionsType; state: StateType }
168 | // type ActionType = ConsumerActionsType
169 | // export { ConsumerType, StateType, ActionType }
170 | ```
171 |
172 | [⇧ back to top](#table-of-contents)
173 |
174 | ### Model Register
175 |
176 | 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.
177 |
178 | `model/index.ts`
179 |
180 | ```typescript
181 | import { Model } from 'react-model'
182 | import Home from '../model/home'
183 | import Shared from '../model/shared'
184 |
185 | const models = { Home, Shared }
186 |
187 | export const { getInitialState, useStore, getState, actions, subscribe, unsubscribe } = Model(models)
188 | ```
189 |
190 | [⇧ back to top](#table-of-contents)
191 |
192 | ### useStore
193 |
194 | The functional component in React ^16.8.0 can use Hooks to connect the global store.
195 | The actions returned from useStore can invoke dom changes.
196 |
197 | The execution of actions returned by useStore will invoke the rerender of current component first.
198 |
199 | It's the only difference between the actions returned by useStore and actions now.
200 |
201 | ```tsx
202 | import React from 'react'
203 | import { useStore } from '../index'
204 |
205 | // CSR
206 | export default () => {
207 | const [state, actions] = useStore('Home')
208 | const [sharedState, sharedActions] = useStore('Shared')
209 |
210 | return (
211 |
212 | Home model value: {JSON.stringify(state)}
213 | Shared model value: {JSON.stringify(sharedState)}
214 |
215 |
218 |
219 |
220 |
221 | )
222 | }
223 | ```
224 |
225 | optional solution on huge dataset (example: TodoList(10000+ Todos)):
226 |
227 | 1. use useStore on the subComponents which need it.
228 | 2. use useStore selector. (version >= v4.0.0-rc.0)
229 |
230 | [advance example with 1000 todo items](https://codesandbox.io/s/react-model-v4-todomvc-oxyij)
231 |
232 | [⇧ back to top](#table-of-contents)
233 |
234 | ### getState
235 |
236 | Key Point: [State variable not updating in useEffect callback](https://github.com/facebook/react/issues/14066)
237 |
238 | To solve it, we provide a way to get the current state of model: getState
239 |
240 | Note: the getState method cannot invoke the dom changes automatically by itself.
241 |
242 | > Hint: The state returned should only be used as readonly
243 |
244 | ```jsx
245 | import { useStore, getState } from '../model/index'
246 |
247 | const BasicHook = () => {
248 | const [state, actions] = useStore('Counter')
249 | useEffect(() => {
250 | console.log('some mounted actions from BasicHooks')
251 | return () =>
252 | console.log(
253 | `Basic Hooks unmounted, current Counter state: ${JSON.stringify(
254 | getState('Counter')
255 | )}`
256 | )
257 | }, [])
258 | return (
259 | <>
260 | state: {JSON.stringify(state)}
261 | >
262 | )
263 | }
264 | ```
265 |
266 | [⇧ back to top](#table-of-contents)
267 |
268 | ### actions
269 |
270 | You can call other models' actions with actions api
271 |
272 | actions can be used in both class components and functional components.
273 |
274 | ```js
275 | import { actions } from './index'
276 |
277 | const model = {
278 | state: {},
279 | actions: {
280 | crossModelCall: () => {
281 | actions.Shared.changeTheme('dark')
282 | actions.Counter.increment(9)
283 | }
284 | }
285 | }
286 |
287 | export default model
288 | ```
289 |
290 | [⇧ back to top](#table-of-contents)
291 |
292 | ### subscribe
293 |
294 | subscribe(storeName, actions, callback) run the callback when the specific actions executed.
295 |
296 | ```typescript
297 | import { subscribe, unsubscribe } from './index'
298 |
299 | const callback = () => {
300 | const user = getState('User')
301 | localStorage.setItem('user_id', user.id)
302 | }
303 |
304 | // subscribe action
305 | subscribe('User', 'login', callback)
306 | // subscribe actions
307 | subscribe('User', ['login', 'logout'], callback)
308 | // unsubscribe the observer of some actions
309 | unsubscribe('User', 'login') // only logout will run callback now
310 | ```
311 |
312 | [⇧ back to top](#table-of-contents)
313 |
314 | ## Advance Concept
315 |
316 | ### immutable Actions
317 |
318 | The actions use [immer](https://github.com/mweststrate/immer) produce API to modify the Store. You can return a producer in action.
319 |
320 | Using function as return value can make your code cleaner when you modify the deep nested value.
321 |
322 | TypeScript Example
323 |
324 | ```ts
325 | // StateType and ActionsParamType definition
326 | // ...
327 |
328 | const model: ModelType = {
329 | actions: {
330 | increment: async (params, { state: s }) => {
331 | // return (state: typeof s) => { // TypeScript < 3.9
332 | return state => {
333 | state.counter += params || 1
334 | }
335 | },
336 | decrease: params => s => {
337 | s.counter += params || 1
338 | }
339 | }
340 | }
341 |
342 | export default model
343 | ```
344 |
345 | JavaScript Example
346 |
347 | ```js
348 | const Model = {
349 | actions: {
350 | increment: async (params) => {
351 | return state => {
352 | state.counter += params || 1
353 | }
354 | }
355 | }
356 | }
357 | ```
358 |
359 | [⇧ back to top](#table-of-contents)
360 |
361 | ### SSR with Next.js
362 |
363 |
364 | Store: shared.ts
365 |
366 |
367 | ```ts
368 | const initialState = {
369 | counter: 0
370 | }
371 |
372 | const model: ModelType = {
373 | actions: {
374 | increment: (params, { state }) => {
375 | return {
376 | counter: state.counter + (params || 1)
377 | }
378 | }
379 | },
380 | // Provide for SSR
381 | asyncState: async context => {
382 | await waitFor(4000)
383 | return { counter: 500 }
384 | },
385 | state: initialState
386 | }
387 |
388 | export default model
389 | ```
390 |
391 |
392 |
393 |
394 |
395 | Global Config: _app.tsx
396 |
397 |
398 |
399 | ```tsx
400 | import { models, getInitialState, Models } from '../model/index'
401 |
402 | let persistModel: any
403 |
404 | interface ModelsProps {
405 | initialModels: Models
406 | persistModel: Models
407 | }
408 |
409 | const MyApp = (props: ModelsProps) => {
410 | if ((process as any).browser) {
411 | // First come in: initialModels
412 | // After that: persistModel
413 | persistModel = props.persistModel || Model(models, props.initialModels)
414 | }
415 | const { Component, pageProps, router } = props
416 | return (
417 |
418 |
419 |
420 | )
421 | }
422 |
423 | MyApp.getInitialProps = async (context: NextAppContext) => {
424 | if (!(process as any).browser) {
425 | const initialModels = context.Component.getInitialProps
426 | ? await context.Component.getInitialProps(context.ctx)
427 | await getInitialState(undefined, { isServer: true }) // get all model initialState
428 | // : await getInitialState({ modelName: 'Home' }, { isServer: true }) // get Home initialState only
429 | // : await getInitialState({ modelName: ['Home', 'Todo'] }, { isServer: true }) // get multi initialState
430 | // : await getInitialState({ data }, { isServer: true }) // You can also pass some public data as asyncData params.
431 | return { initialModels }
432 | } else {
433 | return { persistModel }
434 | }
435 | }
436 | ```
437 |
438 |
439 |
440 |
441 | Page: hooks/index.tsx
442 |
443 |
444 | ```tsx
445 | import { useStore, getState } from '../index'
446 | export default () => {
447 | const [state, actions] = useStore('Home')
448 | const [sharedState, sharedActions] = useStore('Shared')
449 |
450 | return (
451 |
452 | Home model value: {JSON.stringify(state)}
453 | Shared model value: {JSON.stringify(sharedState)}
454 |
460 | )
461 | }
462 | ```
463 |
464 |
465 |
466 |
467 | Single Page Config: benchmark.tsx
468 |
469 |
470 | ```tsx
471 | // ...
472 | Benchmark.getInitialProps = async () => {
473 | return await getInitialState({ modelName: 'Todo' }, { isServer: true })
474 | }
475 | ```
476 |
477 |
478 |
479 | [⇧ back to top](#table-of-contents)
480 |
481 | ### Middleware
482 |
483 | 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.
484 |
485 | ```tsx
486 | // Under the hood
487 | const tryCatch: Middleware<{}> = async (context, restMiddlewares) => {
488 | const { next } = context
489 | await next(restMiddlewares).catch((e: any) => console.log(e))
490 | }
491 |
492 | // ...
493 |
494 | let actionMiddlewares = [
495 | tryCatch,
496 | getNewState,
497 | setNewState,
498 | stateUpdater,
499 | communicator,
500 | devToolsListener
501 | ]
502 |
503 | // ...
504 | // How we execute an action
505 | const consumerAction = (action: Action) => async (params: any) => {
506 | const context: Context = {
507 | modelName,
508 | setState,
509 | actionName: action.name,
510 | next: () => {},
511 | newState: null,
512 | params,
513 | consumerActions,
514 | action
515 | }
516 | await applyMiddlewares(actionMiddlewares, context)
517 | }
518 |
519 | // ...
520 |
521 | export { ... , actionMiddlewares}
522 | ```
523 |
524 | ⚙️ You can override the actionMiddlewares and insert your middleware to specific position
525 |
526 | [⇧ back to top](#table-of-contents)
527 |
528 | ### Expand Context
529 |
530 | ```typescript
531 | const ExtCounter: ModelType<
532 | { name: string }, // State Type
533 | { ext: undefined }, // ActionParamsType
534 | { name: string } // ExtContextType
535 | > = {
536 | actions: {
537 | // { state, action } => { state, action, [name] }
538 | ext: (_, { name }) => {
539 | return { name }
540 | }
541 | },
542 | state: { name: '' }
543 | }
544 |
545 | const { useStore } = Model(ExtCounter, { name: 'test' })
546 | // state.name = ''
547 | const [state, actions] = useStore()
548 | // ...
549 | actions.ext()
550 | // state.name => 'test'
551 | ```
552 |
553 | [⇧ back to top](#table-of-contents)
554 |
555 | ## Other Concept required by Class Component
556 |
557 | ### Provider
558 |
559 | The global state standalone can not effect the react class components, we need to provide the state to react root component.
560 |
561 | ```jsx
562 | import { PureComponent } from 'react'
563 | import { Provider } from 'react-model'
564 |
565 | class App extends PureComponent {
566 | render() {
567 | return (
568 |
569 |
570 |
571 | )
572 | }
573 | }
574 | ```
575 |
576 | [⇧ back to top](#table-of-contents)
577 |
578 | ### connect
579 |
580 | We can use the Provider state with connect.
581 |
582 |
583 | Javascript decorator version
584 |
585 |
586 | ```jsx
587 | import React, { PureComponent } from 'react'
588 | import { Provider, connect } from 'react-model'
589 |
590 | const mapProps = ({ light, counter }) => ({
591 | lightStatus: light ? 'open' : 'close',
592 | counter
593 | }) // You can map the props in connect.
594 |
595 | @connect(
596 | 'Home',
597 | mapProps
598 | )
599 | export default class JSCounter extends PureComponent {
600 | render() {
601 | const { state, actions } = this.props
602 | return (
603 | <>
604 |
states - {JSON.stringify(state)}
605 |
606 |
607 | >
608 | )
609 | }
610 | }
611 | ```
612 |
613 |
614 |
615 |
616 |
617 | TypeScript Version
618 |
619 |
620 | ```tsx
621 | import React, { PureComponent } from 'react'
622 | import { Provider, connect } from 'react-model'
623 | import { StateType, ActionType } from '../model/home'
624 |
625 | const mapProps = ({ light, counter, response }: StateType) => ({
626 | lightStatus: light ? 'open' : 'close',
627 | counter,
628 | response
629 | })
630 |
631 | type RType = ReturnType
632 |
633 | class TSCounter extends PureComponent<
634 | { state: RType } & { actions: ActionType }
635 | > {
636 | render() {
637 | const { state, actions } = this.props
638 | return (
639 | <>
640 | TS Counter
641 | states - {JSON.stringify(state)}
642 |
643 |
644 |
645 | message: {JSON.stringify(state.response)}
646 | >
647 | )
648 | }
649 | }
650 |
651 | export default connect(
652 | 'Home',
653 | mapProps
654 | )(TSCounter)
655 | ```
656 |
657 |
658 |
659 | [⇧ back to top](#table-of-contents)
660 |
661 | ## FAQ
662 |
663 | ### Migrate from 3.1.x to 4.x.x
664 |
665 | 1. remove Model wrapper
666 |
667 | `sub-model.ts`
668 | ```ts
669 | // 3.1.x
670 | export default Model(model)
671 | // 4.x.x
672 | export default model
673 | ```
674 |
675 | `models.ts`
676 | ```ts
677 | import Sub from './sub-model'
678 | export default Model({ Sub })
679 | ```
680 |
681 | 2. use selector to replace depActions
682 |
683 | `Shared.ts`
684 | ```ts
685 | interface State {
686 | counter: number
687 | enable: boolean
688 | }
689 |
690 | interface ActionParams {
691 | add: number
692 | switch: undefined
693 | }
694 |
695 | const model: ModelType = {
696 | state: {
697 | counter: 1
698 | enable: false
699 | },
700 | actions: {
701 | add: (payload) => state => {
702 | state.counter += payload
703 | },
704 | switch: () => state => {
705 | state.enable = !state.enable
706 | }
707 | }
708 | }
709 | ```
710 |
711 | ```ts
712 | const Component = () => {
713 | // 3.1.x, Component rerender when add action is invoked
714 | const [counter] = useStore('Shared', ['add'])
715 | // 4.x.x, Component rerender when counter value diff
716 | const [counter] = useStore('Shared', state => state.counter)
717 | }
718 | ```
719 |
720 | ### How can I disable the console debugger
721 |
722 |
723 | ```typescript
724 | import { middlewares } from 'react-model'
725 | // Find the index of middleware
726 |
727 | // Disable all actions' log
728 | middlewares.config.logger.enable = false
729 | // Disable logs from specific type of actions
730 | middlewares.config.logger.enable = ({ actionName }) => ['increment'].indexOf(actionName) !== -1
731 | ```
732 |
733 | [⇧ back to top](#table-of-contents)
734 |
735 | ### How can I add custom middleware
736 |
737 | ```typescript
738 | import { actionMiddlewares, middlewares, Model } from 'react-model'
739 | import { sendLog } from 'utils/log'
740 | import Home from '../model/home'
741 | import Shared from '../model/shared'
742 |
743 | // custom middleware
744 | const ErrorHandler: Middleware = async (context, restMiddlewares) => {
745 | const { next } = context
746 | await next(restMiddlewares).catch((e: Error) => sendLog(e))
747 | }
748 |
749 | // Find the index of middleware
750 | const getNewStateMiddlewareIndex = actionMiddlewares.indexOf(
751 | middlewares.getNewState
752 | )
753 |
754 | // Replace it
755 | actionMiddlewares.splice(getNewStateMiddlewareIndex, 0, ErrorHandler)
756 |
757 | const stores = { Home, Shared }
758 |
759 | export default Model(stores)
760 | ```
761 |
762 | [⇧ back to top](#table-of-contents)
763 |
764 | #### How can I make persist models
765 |
766 | ```typescript
767 | import { actionMiddlewares, Model } from 'react-model'
768 | import Example from 'models/example'
769 |
770 | // Example, not recommend to use on production directly without consideration
771 | // Write current State to localStorage after action finish
772 | const persistMiddleware: Middleware = async (context, restMiddlewares) => {
773 | localStorage.setItem('__REACT_MODEL__', JSON.stringify(context.Global.State))
774 | await context.next(restMiddlewares)
775 | }
776 |
777 | // Use on all models
778 | actionMiddlewares.push(persistMiddleware)
779 | Model({ Example }, JSON.parse(localStorage.getItem('__REACT_MODEL__')))
780 |
781 | // Use on single model
782 | const model = {
783 | state: JSON.parse(localStorage.getItem('__REACT_MODEL__'))['you model name']
784 | actions: { ... },
785 | middlewares: [...actionMiddlewares, persistMiddleware]
786 | }
787 |
788 |
789 | ```
790 |
791 | [⇧ back to top](#table-of-contents)
792 |
793 | ### How can I deal with local state
794 |
795 | What should I do to make every Counter hold there own model? 🤔
796 |
797 | ```tsx
798 | class App extends Component {
799 | render() {
800 | return (
801 |
802 |
803 |
804 |
805 |
806 | )
807 | }
808 | }
809 | ```
810 |
811 |
812 | Counter model
813 |
814 |
815 | ```ts
816 | interface State {
817 | count: number
818 | }
819 |
820 | interface ActionParams {
821 | increment: number
822 | }
823 |
824 | const model: ModelType = {
825 | state: {
826 | count: 0
827 | },
828 | actions: {
829 | increment: payload => {
830 | // 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
831 | // Not allowed
832 | // return state => (state.count += payload)
833 | return state => {
834 | state.count += payload
835 | }
836 | }
837 | }
838 | }
839 |
840 | ```
841 |
842 |
843 |
844 |
845 |
846 | Counter.tsx
847 |
848 |
849 | ```tsx
850 |
851 | const Counter = () => {
852 | const [{ useStore }] = useState(() => Model(model))
853 | const [state, actions] = useStore()
854 | return (
855 |
856 |
{state.count}
857 |
858 |
859 | )
860 | }
861 |
862 | export default Counter
863 | ```
864 |
865 |
866 |
867 |
868 | [⇧ back to top](#table-of-contents)
869 |
870 | ### actions throw error from immer.module.js
871 |
872 | ```
873 | 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
874 | ```
875 |
876 | How to fix:
877 |
878 | ```tsx
879 | actions: {
880 | increment: payload => {
881 | // Not allowed
882 | // return state => (state.count += payload)
883 | return state => {
884 | state.count += payload
885 | }
886 | }
887 | }
888 | ```
889 |
890 | [⇧ back to top](#table-of-contents)
891 |
892 | ### How can I customize each model's middlewares?
893 |
894 | You can customize each model's middlewares.
895 |
896 | ```typescript
897 | import { actionMiddlewares, Model } from 'react-model'
898 | const delayMiddleware: Middleware = async (context, restMiddlewares) => {
899 | await timeout(1000, {})
900 | context.next(restMiddlewares)
901 | }
902 |
903 | const nextCounterModel: ModelType = {
904 | actions: {
905 | add: num => {
906 | return state => {
907 | state.count += num
908 | }
909 | },
910 | increment: async (num, { actions }) => {
911 | actions.add(num)
912 | await timeout(300, {})
913 | }
914 | },
915 | // You can define the custom middlewares here
916 | middlewares: [delayMiddleware, ...actionMiddlewares],
917 | state: {
918 | count: 0
919 | }
920 | }
921 |
922 | export default Model(nextCounterModel)
923 | ```
924 |
925 | [⇧ back to top](#table-of-contents)
926 |
--------------------------------------------------------------------------------
/__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/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 SSRCounterState = {
6 | count: number
7 | clientKey: string
8 | }
9 |
10 | type ExtState = {
11 | name: string
12 | }
13 |
14 | type ThemeState = {
15 | theme: 'dark' | 'light'
16 | }
17 |
18 | type ActionTesterState = {
19 | response: { data: Object }
20 | data: Object
21 | }
22 |
23 | type CounterActionParams = {
24 | increment: number
25 | }
26 |
27 | type ExtActionParams = {
28 | ext: undefined
29 | }
30 |
31 | type NextCounterActionParams = {
32 | increment: number
33 | add: number
34 | }
35 |
36 | type ExtraActionParams = {
37 | add: number
38 | addCaller: undefined
39 | }
40 |
41 | type ThemeActionParams = {
42 | changeTheme: 'dark' | 'light'
43 | }
44 |
45 | type ActionTesterParams = {
46 | get: undefined
47 | parse: undefined
48 | getData: undefined
49 | }
50 |
--------------------------------------------------------------------------------
/__test__/index.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | import { Model } from '../src'
4 | import { timeout } from '../src/helper'
5 | import { actionMiddlewares } from '../src/middlewares'
6 |
7 | export const ActionsTester: ModelType = {
8 | actions: {
9 | get: async () => {
10 | const response = await timeout(9, { code: 0, data: { counter: 1000 } })
11 | return { response }
12 | },
13 | getData: async (_, { actions }) => {
14 | await actions.get()
15 | actions.parse()
16 | },
17 | parse: () => {
18 | return state => {
19 | state.data = state.response.data
20 | }
21 | }
22 | },
23 | state: {
24 | data: {},
25 | response: {
26 | data: {}
27 | }
28 | }
29 | }
30 |
31 | export const Counter: ModelType<
32 | CounterState,
33 | CounterActionParams & ExtraActionParams
34 | > = {
35 | actions: {
36 | add: (params, { state }) => {
37 | return {
38 | count: state.count + params
39 | }
40 | },
41 | addCaller: (_, { actions }) => {
42 | actions.add(5)
43 | },
44 | increment: params => {
45 | return state => {
46 | state.count += params
47 | }
48 | }
49 | },
50 | state: { count: 0 }
51 | }
52 |
53 | // v3.0
54 | export const NextCounter: ModelType<
55 | CounterState,
56 | CounterActionParams & ExtraActionParams
57 | > = {
58 | actions: {
59 | add: (params, { state }) => {
60 | return {
61 | count: state.count + params
62 | }
63 | },
64 | addCaller: (_, { actions }) => {
65 | actions.add(5)
66 | },
67 | increment: params => {
68 | return state => {
69 | state.count += params
70 | }
71 | }
72 | },
73 | state: { count: 0 }
74 | }
75 |
76 | export const ExtCounter: ModelType<
77 | ExtState,
78 | ExtActionParams,
79 | { name: string }
80 | > = {
81 | actions: {
82 | ext: (_, { name }) => {
83 | return {
84 | name
85 | }
86 | }
87 | },
88 | state: { name: '' }
89 | }
90 |
91 | export const Theme: ModelType = {
92 | actions: {
93 | changeTheme: (_, { state }) => ({
94 | theme: state.theme === 'dark' ? 'light' : 'dark'
95 | })
96 | },
97 | state: {
98 | theme: 'dark'
99 | }
100 | }
101 |
102 | export const AsyncCounter: ModelType = {
103 | actions: {
104 | increment: params => {
105 | return state => {
106 | state.count += params
107 | }
108 | }
109 | },
110 | asyncState: async (context: { count?: number }) => ({
111 | count: context ? context.count || 1 : 1
112 | }),
113 | state: { count: 0 }
114 | }
115 |
116 | export const SSRCounter: ModelType = {
117 | actions: {
118 | increment: params => {
119 | return state => {
120 | state.count += params
121 | }
122 | }
123 | },
124 | asyncState: async (context: { count?: number }) => ({
125 | count: context ? context.count || 1 : 1
126 | }),
127 | state: { count: 0, clientKey: 'unused' }
128 | }
129 |
130 | export const AsyncNull: ModelType = {
131 | actions: {
132 | increment: params => {
133 | return state => {
134 | state.count += params
135 | }
136 | }
137 | },
138 | state: { count: 0 }
139 | }
140 |
141 | const timeoutCounter: ModelType = {
142 | actions: {
143 | increment: async (params, { state: _ }) => {
144 | await timeout(4000, {})
145 | return (state: typeof _) => {
146 | state.count += params
147 | }
148 | }
149 | },
150 | asyncState: async () => ({
151 | count: 1
152 | }),
153 | state: { count: 0 }
154 | }
155 |
156 | export const TimeoutCounter = Model(timeoutCounter)
157 |
158 | export const ErrorCounter: ModelType = {
159 | actions: {
160 | increment: async () => {
161 | throw 'error'
162 | }
163 | },
164 | state: { count: 0 }
165 | }
166 |
167 | const delayMiddleware: Middleware = async (context, restMiddlewares) => {
168 | await timeout(1000, {})
169 | context.next(restMiddlewares)
170 | }
171 |
172 | export const NextCounterModel: ModelType<
173 | CounterState,
174 | NextCounterActionParams
175 | > = {
176 | actions: {
177 | add: num => {
178 | return state => {
179 | state.count += num
180 | }
181 | },
182 | increment: async (num, { actions }) => {
183 | actions.add(num)
184 | await timeout(300, {})
185 | }
186 | },
187 | middlewares: [delayMiddleware, ...actionMiddlewares],
188 | state: {
189 | count: 0
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/__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 } from '@testing-library/react-hooks'
7 | import { Model, middlewares } 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__/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 | const Counter = Model(NextCounter)
11 | const { useStore, subscribe } = Model({ Counter })
12 | subscribe('Counter', ['increment'], () => (count += 1))
13 | subscribe('Counter', 'add', () => (count += 10))
14 | subscribe('Counter', ['increment', 'add'], () => (count += 5))
15 | renderHook(() => {
16 | ;[, actions] = useStore('Counter')
17 | })
18 | await actions.increment()
19 | await actions.add(1)
20 | await actions.increment()
21 | await actions.increment()
22 | expect(count).toBe(33)
23 | await actions.addCaller()
24 | expect(count).toBe(48)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/__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__/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 | // "**/__tests__/**/*.[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.0.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": "microbundle --sourcemap false --jsx React.createElement --output dist --tsconfig ./tsconfig.json",
11 | "commit": "git-cz",
12 | "lint-ts": "tslint -c tslint.json 'src/**/*.ts'",
13 | "lint-md": "remark .",
14 | "test": "jest --silent",
15 | "test:coverage": "jest --collect-coverage --silent"
16 | },
17 | "keywords": ["react", "model", "state-management", "react-hooks"],
18 | "author": "ArrayZoneYour ",
19 | "license": "MIT",
20 | "dependencies": {
21 | "immer": ">=8.0.1"
22 | },
23 | "peerDependencies": {
24 | "react": ">=16.3.0",
25 | "react-dom": ">=16.3.0"
26 | },
27 | "devDependencies": {
28 | "@commitlint/cli": "^8.1.0",
29 | "@commitlint/config-conventional": "^8.1.0",
30 | "@testing-library/react": "^10.0.1",
31 | "@testing-library/react-hooks": "^2.0.1",
32 | "@types/babel__core": "^7.1.1",
33 | "@types/babel__template": "^7.0.2",
34 | "@types/jest": "^25.1.0",
35 | "@types/node": "^14.0.0",
36 | "@types/react": "^16.9.1",
37 | "@types/react-dom": "^16.8.0",
38 | "commitizen": "^4.0.0",
39 | "cz-conventional-changelog": "^3.0.0",
40 | "husky": "^4.0.2",
41 | "jest": "^24.1.0",
42 | "microbundle": "^0.12.3",
43 | "prettier": "^2.0.0",
44 | "react": "^16.8.4",
45 | "react-dom": "^16.8.4",
46 | "react-test-renderer": "^16.8.6",
47 | "remark-cli": "^8.0.0",
48 | "remark-lint": "^7.0.0",
49 | "remark-preset-lint-recommended": "^4.0.0",
50 | "ts-jest": "^26.0.0",
51 | "tslint": "^5.14.0",
52 | "typescript": "^3.4.5"
53 | },
54 | "husky": {
55 | "hooks": {
56 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
57 | }
58 | },
59 | "repository": {
60 | "type": "git",
61 | "url": "git+https://github.com/byte-fe/react-model"
62 | },
63 | "bugs": {
64 | "url": "https://github.com/byte-fe/react-model/issues"
65 | },
66 | "homepage": "https://github.com/byte-fe/react-model#readme",
67 | "config": {
68 | "commitizen": {
69 | "path": "./node_modules/cz-conventional-changelog"
70 | }
71 | },
72 | "browserslist": [
73 | "edge 17",
74 | "firefox 70",
75 | "chrome 48",
76 | "safari 12.1",
77 | "android 4.0",
78 | "samsung 9.2"
79 | ]
80 | }
81 |
--------------------------------------------------------------------------------
/src/global.ts:
--------------------------------------------------------------------------------
1 | const State = {}
2 | const Actions = {}
3 | const AsyncState = {}
4 | const Middlewares = {}
5 | // Communicate between Provider-Consumer and Hooks
6 | const Setter: Setter = {
7 | // classSetter stores the setState from Provider
8 | // Invoke the classSetter.setState can update the state of Global Provider.
9 | classSetter: undefined,
10 | // functionSetter stores the setState returned by useStore.
11 | // These setStates can invoke the rerender of hooks components.
12 | functionSetter: {}
13 | }
14 |
15 | const Context = {
16 | __global: {}
17 | }
18 |
19 | const subscriptions = {}
20 |
21 | let devTools: any
22 | let withDevTools = false
23 |
24 | let uid = 0 // The unique id of hooks
25 |
26 | export default {
27 | Actions,
28 | AsyncState,
29 | Context,
30 | Middlewares,
31 | Setter,
32 | State,
33 | devTools,
34 | subscriptions,
35 | uid,
36 | withDevTools
37 | } as Global
38 |
--------------------------------------------------------------------------------
/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: 'outer'
37 | }
38 | 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 timeout = (ms: number, data: T): Promise =>
77 | new Promise((resolve) =>
78 | setTimeout(() => {
79 | console.log(ms)
80 | resolve(data)
81 | }, ms)
82 | )
83 |
84 | const getInitialState = async (
85 | context?: T,
86 | config?: { isServer?: boolean }
87 | ) => {
88 | const ServerState: { [name: string]: any } = { __FROM_SERVER__: true }
89 | await Promise.all(
90 | Object.keys(Global.State).map(async (modelName) => {
91 | if (
92 | !context ||
93 | !context.modelName ||
94 | modelName === context.modelName ||
95 | context.modelName.indexOf(modelName) !== -1
96 | ) {
97 | const asyncGetter = Global.AsyncState[modelName]
98 | const asyncState = asyncGetter ? await asyncGetter(context) : {}
99 | if (config && config.isServer) {
100 | ServerState[modelName] = asyncState
101 | } else {
102 | Global.State = produce(Global.State, (s) => {
103 | s[modelName] = { ...s[modelName], ...asyncState }
104 | })
105 | }
106 | }
107 | })
108 | )
109 | return config && config.isServer ? ServerState : Global.State
110 | }
111 |
112 | const getCache = (modelName: string, actionName: string) => {
113 | const JSONString = localStorage.getItem(
114 | `__REACT_MODELX__${modelName}_${actionName}`
115 | )
116 | return JSONString ? JSON.parse(JSONString) : null
117 | }
118 |
119 | const shallowEqual = (objA: any, objB: any) => {
120 | if (objA === objB) return true
121 | if (
122 | typeof objA !== 'object' ||
123 | objA === null ||
124 | typeof objB !== 'object' ||
125 | objB === null
126 | ) {
127 | return false
128 | }
129 | const keysA = Object.keys(objA)
130 | const keysB = Object.keys(objB)
131 |
132 | if (keysA.length !== keysB.length) return false
133 |
134 | for (let i = 0; i < keysA.length; i++) {
135 | if (
136 | !Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
137 | objA[keysA[i]] !== objB[keysA[i]]
138 | ) {
139 | return false
140 | }
141 | }
142 |
143 | return true
144 | }
145 |
146 | export {
147 | Consumer,
148 | consumerActions,
149 | GlobalContext,
150 | setPartialState,
151 | shallowEqual,
152 | timeout,
153 | getCache,
154 | getInitialState
155 | }
156 |
--------------------------------------------------------------------------------
/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 | AsyncState: {
32 | [modelName: string]: undefined | ((context?: any) => Promise>)
33 | }
34 | Context: any
35 | Middlewares: {
36 | [modelName: string]: Middleware[]
37 | }
38 | subscriptions: Subscriptions
39 | Setter: Setter
40 | devTools: any
41 | withDevTools: boolean
42 | uid: number
43 | }
44 |
45 | type ClassSetter = React.Dispatch | undefined
46 |
47 | // Very Sad, Promise> can not work with Partial | ProduceFunc
48 | type Action = (
49 | params: P,
50 | context: {
51 | state: S
52 | actions: getConsumerActionsType>
53 | } & ExtContext
54 | ) =>
55 | | Partial
56 | | Promise>
57 | | ProduceFunc
58 | | Promise>
59 | | void
60 | | Promise
61 |
62 | type ProduceFunc = (state: S) => void
63 |
64 | // v3.0 Actions
65 | type Actions = {
66 | [P in keyof ActionKeys]: Action
67 | }
68 |
69 | type Dispatch = (value: A) => void
70 | type SetStateAction = S | ((prevState: S) => S)
71 |
72 | interface ModelContext {
73 | modelName: string
74 | }
75 |
76 | interface BaseContext {
77 | action: Action
78 | consumerActions: (
79 | actions: Actions,
80 | modelContext: ModelContext
81 | ) => getConsumerActionsType
82 | params: Object
83 | middlewareConfig?: Object
84 | actionName: string
85 | modelName: string
86 | next?: Function
87 | newState: Global['State'] | Function | null
88 | Global: Global
89 | }
90 |
91 | interface InnerContext extends BaseContext {
92 | // Actions with function type context will always invoke current component's reload.
93 | type?: 'function' | 'outer' | 'class'
94 | __hash?: string
95 | }
96 |
97 | type Context = InnerContext & {
98 | next: Function
99 | modelMiddlewares?: Middleware[]
100 | }
101 |
102 | type Middleware = (C: Context, M: Middleware[]) => void
103 |
104 | type MiddlewareConfig = {
105 | logger: {
106 | enable: boolean | ((context: BaseContext) => boolean)
107 | }
108 | devtools: { enable: boolean }
109 | tryCatch: { enable: boolean }
110 | }
111 |
112 | interface Models {
113 | [name: string]:
114 | | ModelType
115 | | API>
116 | }
117 |
118 | type Selector = (state: S) => R
119 |
120 | interface API> {
121 | __id: string
122 | __ERROR__?: boolean
123 | useStore: <
124 | F extends Selector, any> = Selector<
125 | Get,
126 | unknown
127 | >
128 | >(
129 | selector?: F
130 | ) => [
131 | F extends Selector, any>
132 | ? Equals, unknown>> extends true
133 | ? Get
134 | : ReturnType
135 | : Get,
136 | getConsumerActionsType>
137 | ]
138 | getState: () => Readonly>
139 | subscribe: (
140 | actionName: keyof MT['actions'] | Array,
141 | callback: () => void
142 | ) => void
143 | unsubscribe: (
144 | actionName: keyof Get | Array>
145 | ) => void
146 | actions: Readonly>>
147 | }
148 |
149 | interface APIs {
150 | useStore: <
151 | K extends keyof M,
152 | S extends M[K] extends API
153 | ? ArgumentTypes>[1]
154 | : M[K] extends ModelType
155 | ? Selector, unknown>
156 | : any
157 | >(
158 | name: K,
159 | selector?: S
160 | ) => M[K] extends API
161 | ? S extends (...args: any) => void
162 | ? Equals extends true
163 | ? [ReturnType>, Get]
164 | : ReturnType
165 | : ReturnType>
166 | : M[K] extends ModelType
167 | ? S extends (...args: any) => void
168 | ? [
169 | Equals, unknown> extends true
170 | ? Get
171 | : ReturnType,
172 | getConsumerActionsType>
173 | ]
174 | : [Get, getConsumerActionsType>]
175 | : any
176 |
177 | getState: (
178 | modelName: K
179 | ) => M[K] extends ModelType
180 | ? Readonly>
181 | : M[K] extends API
182 | ? ReturnType>
183 | : any
184 | getActions: (
185 | modelName: K
186 | ) => M[K] extends ModelType
187 | ? Readonly>>
188 | : M[K] extends API
189 | ? M[K]['actions']
190 | : unknown
191 | getInitialState: (
192 | context?: T | undefined,
193 | config?: { isServer: boolean }
194 | ) => Promise<{
195 | [modelName: string]: any
196 | }>
197 | subscribe: (
198 | modelName: K,
199 | actionName: keyof Get | Array>,
200 | callback: () => void
201 | ) => void
202 | unsubscribe: (
203 | modelName: K,
204 | actionName: keyof Get | Array>
205 | ) => void
206 | actions: {
207 | [K in keyof M]: M[K] extends API
208 | ? M[K]['actions']
209 | : Readonly>>
210 | }
211 | }
212 |
213 | // v3.0
214 | type ModelType<
215 | InitStateType = any,
216 | ActionKeys = any,
217 | ExtContext extends {} = {}
218 | > = {
219 | __ERROR__?: boolean
220 | actions: {
221 | [P in keyof ActionKeys]: Action<
222 | InitStateType,
223 | ActionKeys[P],
224 | ActionKeys,
225 | ExtContext
226 | >
227 | }
228 | middlewares?: Middleware[]
229 | state: InitStateType
230 | asyncState?: (context?: any) => Promise>
231 | }
232 |
233 | type ArgumentTypes = F extends (...args: infer A) => any
234 | ? A
235 | : never
236 |
237 | // v3.0
238 | // TODO: ArgumentTypes[0] = undefined | string
239 | type getConsumerActionsType> = {
240 | [P in keyof A]: ArgumentTypes[0] extends undefined
241 | ? (params?: ArgumentTypes[0]) => ReturnType
242 | : (params: ArgumentTypes[0]) => ReturnType
243 | }
244 |
245 | type Get