├── .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 · ![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg) [![npm version](https://img.shields.io/npm/v/react-model.svg?style=flat)](https://www.npmjs.com/package/react-model) [![minified size](https://badgen.net/bundlephobia/min/react)](https://bundlephobia.com/result?p=react-model) [![Build Status](https://travis-ci.org/byte-fe/react-model.svg?branch=master)](https://travis-ci.org/byte-fe/react-model) [![size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/react-model/dist/react-model.js?compression=gzip)](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/react-model/dist/react-model.js) [![downloads](https://img.shields.io/npm/dt/react-model.svg)](https://www.npmjs.com/package/react-model) [![Coverage Status](https://codecov.io/gh/byte-fe/react-model/branch/master/graph/badge.svg)](https://codecov.io/gh/byte-fe/react-model) [![Greenkeeper badge](https://badges.greenkeeper.io/byte-fe/react-model.svg)](https://greenkeeper.io/) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg) 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 | 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 | 26 | ) 27 | } 28 | } 29 | ) 30 | 31 | describe('class component', () => { 32 | test('render props', () => { 33 | Model({ Counter }) 34 | const { container } = render( 35 | 36 | 23 | ) 24 | } 25 | } 26 | ) 27 | 28 | describe('class component', () => { 29 | test('render props', () => { 30 | Model({ Counter }) 31 | const { container } = render( 32 | 33 | 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 |