├── .eslintignore ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.en.md ├── README.md ├── codecov.yml ├── commitlint.config.js ├── docs ├── api.md ├── qna.md ├── recipes.md └── upgrade-guidelines.md ├── examples ├── classComponent │ ├── .gitgnore │ ├── build.json │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── components │ │ │ ├── Product │ │ │ │ ├── Class.tsx │ │ │ │ ├── Function.tsx │ │ │ │ ├── Product.tsx │ │ │ │ ├── index.ts │ │ │ │ └── model.ts │ │ │ └── User │ │ │ │ ├── Class.tsx │ │ │ │ ├── Function.tsx │ │ │ │ ├── User.tsx │ │ │ │ └── index.ts │ │ ├── index.tsx │ │ ├── models │ │ │ ├── index.ts │ │ │ └── user.ts │ │ ├── react-app-env.d.ts │ │ └── store.ts │ └── tsconfig.json ├── counter │ ├── .gitgnore │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── index.tsx │ │ └── react-app-env.d.ts │ └── tsconfig.json ├── migration-redux-1 │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.js │ │ ├── index.js │ │ └── reducers │ │ ├── dolphins.js │ │ └── sharks.js ├── migration-redux-2 │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.js │ │ ├── index.js │ │ ├── models │ │ └── sharks.js │ │ └── reducers │ │ └── dolphins.js ├── migration-redux-3 │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.js │ │ ├── index.js │ │ ├── models │ │ ├── dolphins.js │ │ └── sharks.js │ │ ├── redux.js │ │ └── store.js ├── migration-redux-4 │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── App.js │ │ ├── index.js │ │ ├── models │ │ ├── dolphins.js │ │ └── sharks.js │ │ ├── redux.js │ │ └── store.js ├── todos │ ├── .gitignore │ ├── package.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── components │ │ │ ├── AddTodo.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Todo.tsx │ │ │ └── TodoList.tsx │ │ ├── index.tsx │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── todos.ts │ │ │ └── visibilityFilter.ts │ │ ├── react-app-env.d.ts │ │ ├── store.ts │ │ └── utils.ts │ └── tsconfig.json └── withModel │ ├── .gitgnore │ ├── package.json │ ├── public │ └── index.html │ ├── src │ ├── index.tsx │ └── react-app-env.d.ts │ └── tsconfig.json ├── package.json ├── scripts ├── checkVersionExist.ts └── publish.ts ├── src ├── actionTypes.ts ├── icestore.ts ├── index.tsx ├── pluginFactory.ts ├── plugins │ ├── dispatch.ts │ ├── effects.ts │ ├── error.tsx │ ├── immer.ts │ ├── loading.tsx │ ├── modelApis.tsx │ ├── provider.tsx │ └── reduxHooks.ts ├── redux.ts ├── types.ts └── utils │ ├── appendReducers.ts │ ├── checkModels.ts │ ├── isListener.ts │ ├── mergeConfig.ts │ └── validate.ts ├── tests ├── .eslintrc ├── helpers │ ├── CounterClassComponent.tsx │ ├── counter.ts │ ├── createHook.tsx │ └── todos.ts ├── index.spec.tsx ├── plugins │ ├── Provider.spec.tsx │ ├── dispatch.spec.tsx │ ├── effects.spec.tsx │ ├── error.spec.tsx │ ├── immer.spec.tsx │ ├── loading.spec.tsx │ └── modelApis.spec.tsx └── utils │ ├── appendReducer.spec.ts │ ├── check.spec.ts │ └── validate.spec.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | esm 3 | node_modules -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const { getESLintConfig } = require('@applint/spec'); 2 | 3 | module.exports = getESLintConfig('common-ts', { 4 | env: { 5 | jest: true, 6 | }, 7 | settings: { 8 | react: { 9 | pragma: 'React', 10 | version: 'detect', 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | about: Report a reproducible bug or regression. 4 | title: 'Bug: ' 5 | labels: 'bug' 6 | 7 | --- 8 | 9 | 14 | 15 | @ice/store version: 16 | 17 | ## Steps To Reproduce 18 | 19 | 1. 20 | 2. 21 | 22 | 27 | 28 | Link to code example: 29 | 30 | 37 | 38 | ## The current behavior 39 | 40 | 41 | ## The expected behavior 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🚀 Feature Request" 3 | about: Suggest an idea for icestore 4 | title: 'Feature: ' 5 | labels: 'enhancement' 6 | 7 | --- 8 | 9 | ### Problem Description 10 | 11 | 12 | ### Proposed Solution 13 | 14 | 15 | ### Alternatives Considered 16 | 17 | 18 | ### Additional Information 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - name: Use Node.js 12.x 9 | uses: actions/setup-node@v2 10 | with: 11 | node-version: 12.x 12 | registry-url: https://registry.npmjs.org 13 | - name: Set branch name 14 | run: echo >>$GITHUB_ENV BRANCH_NAME=${GITHUB_REF#refs/heads/} 15 | - name: Output branch name 16 | run: echo ${BRANCH_NAME} 17 | - run: npm install 18 | - run: npm run lint 19 | - run: npm run build 20 | - run: npm run test 21 | - name: Generate coverage 22 | run: npm run coverage 23 | - name: Upload coverage to Codecov 24 | uses: codecov/codecov-action@v1 25 | - name: Publish And Notify 26 | run: npm run publish 27 | env: 28 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 29 | DING_WEBHOOK: ${{secrets.DING_WEBHOOK}} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional eslint cache 43 | .eslintcache 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # next.js build output 58 | .next 59 | 60 | # Compile directory 61 | lib 62 | esm 63 | 64 | package-lock.json 65 | 66 | yarn.lock 67 | 68 | .ice 69 | .DS_Store 70 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 2, 4 | semi: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | See https://github.com/ice-lab/icestore/releases -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2019 ICE Team 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README.md) 2 | 3 | # icestore 4 | 5 | > Simple and friendly state for React. 6 | 7 | [![NPM version](https://img.shields.io/npm/v/@ice/store.svg?style=flat)](https://npmjs.org/package/@ice/store) 8 | [![build status](https://github.com/ice-lab/icestore/actions/workflows/ci.yml/badge.svg)](https://github.com/ice-lab/icestore/actions/workflows/ci.yml) 9 | [![NPM downloads](http://img.shields.io/npm/dm/@ice/store.svg?style=flat)](https://npmjs.org/package/@ice/store) 10 | [![codecov](https://codecov.io/gh/ice-lab/icestore/branch/master/graph/badge.svg)](https://codecov.io/gh/ice-lab/icestore) 11 | 12 | ## Versions 13 | 14 | | Version | Branch | Docs | 15 | | --- | --- | --- | 16 | | V2 | master | [Docs](https://github.com/ice-lab/icestore#文档) 17 | | V1 | stable/1.x | [Docs](https://github.com/ice-lab/icestore/tree/stable/1.x#documents) 18 | 19 | ## Introduction 20 | 21 | icestore is a simple and friendly state management library for React. It has the following core features: 22 | 23 | - **Minimal & Familiar API**: No additional learning costs, easy to get started with the knowledge of Redux && React Hooks. 24 | - **Built in Async Status**: Records loading and error status of effects, simplifying the rendering logic in the view layer. 25 | - **Class Component Support**: Make old projects enjoying the fun of lightweight state management with friendly compatibility strategy. 26 | - **TypeScript Support**: Provide complete type definitions to support intelliSense in VS Code. 27 | 28 | See the [comparison table](docs/recipes.md#能力对比表) for more details. 29 | 30 | ## Documents 31 | 32 | - [API](./docs/api.md) 33 | - [Recipes](./docs/recipes.md) 34 | - [Upgrade from V1](./docs/upgrade-guidelines.md) 35 | - [Q & A](./docs/qna.md) 36 | 37 | ## Examples 38 | 39 | - [Counter](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/counter) 40 | - [Todos](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/todos) 41 | - [Class Component Support](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/classComponent) 42 | - [withModel](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/withModel) 43 | 44 | ## Basic example 45 | 46 | ```jsx 47 | import React from 'react'; 48 | import ReactDOM from 'react-dom'; 49 | import { createStore, createModel } from '@ice/store'; 50 | 51 | const delay = (time) => 52 | new Promise((resolve) => setTimeout(() => resolve(), time)); 53 | 54 | // 1️⃣ Use a model to define your store 55 | const counter = createModel({ 56 | state: 0, 57 | reducers: { 58 | increment: (prevState) => prevState + 1, 59 | decrement: (prevState) => prevState - 1, 60 | }, 61 | effects: () => ({ 62 | async asyncDecrement() { 63 | await delay(1000); 64 | this.decrement(); 65 | }, 66 | }), 67 | }); 68 | 69 | const models = { 70 | counter, 71 | }; 72 | 73 | // 2️⃣ Create the store 74 | const store = createStore(models); 75 | 76 | // 3️⃣ Consume model 77 | const { useModel } = store; 78 | function Counter() { 79 | const [count, dispatchers] = useModel('counter'); 80 | const { increment, asyncDecrement } = dispatchers; 81 | return ( 82 |
83 | {count} 84 | 87 | 90 |
91 | ); 92 | } 93 | 94 | // 4️⃣ Wrap your components with Provider 95 | const { Provider } = store; 96 | function App() { 97 | return ( 98 | 99 | 100 | 101 | ); 102 | } 103 | 104 | const rootElement = document.getElementById('root'); 105 | ReactDOM.render(, rootElement); 106 | ``` 107 | 108 | ## Installation 109 | 110 | icestore requires React 16.8.0 or later. 111 | 112 | ```bash 113 | npm install @ice/store --save 114 | ``` 115 | 116 | ## Inspiration 117 | 118 | icestore refines and builds upon the ideas of [rematch](https://github.com/rematch/rematch) & [constate](https://github.com/diegohaz/constate). 119 | 120 | ## Contributors 121 | 122 | Feel free to report any questions as an [issue](https://github.com/ice-lab/icestore/issues/new), we'd love to have your helping hand on icestore. 123 | 124 | Develop: 125 | 126 | ```bash 127 | $ cd icestore/ 128 | $ npm install 129 | $ npm run test 130 | $ npm run watch 131 | 132 | $ cd examples/counter 133 | $ npm install 134 | $ npm link ../../ # link icestore 135 | $ npm link ../../node_modules/react # link react 136 | $ npm start 137 | ``` 138 | 139 | ## License 140 | 141 | [MIT](LICENSE) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 简体中文 | [English](./README.en.md) 2 | 3 | # icestore 4 | 5 | > 简单友好的状态管理方案。 6 | 7 | [![NPM version](https://img.shields.io/npm/v/@ice/store.svg?style=flat)](https://npmjs.org/package/@ice/store) 8 | [![build status](https://github.com/ice-lab/icestore/actions/workflows/ci.yml/badge.svg)](https://github.com/ice-lab/icestore/actions/workflows/ci.yml) 9 | [![NPM downloads](http://img.shields.io/npm/dm/@ice/store.svg?style=flat)](https://npmjs.org/package/@ice/store) 10 | [![codecov](https://codecov.io/gh/ice-lab/icestore/branch/master/graph/badge.svg)](https://codecov.io/gh/ice-lab/icestore) 11 | 12 | ## 版本 13 | 14 | | 版本 | 代码分支 | 文档 | 15 | | --- | --- | --- | 16 | | V2 | master | [Docs](https://github.com/ice-lab/icestore#文档) 17 | | V1 | stable/1.x | [Docs](https://github.com/ice-lab/icestore/tree/stable/1.x#documents) 18 | 19 | ## 介绍 20 | 21 | icestore 是面向 React 应用的、简单友好的状态管理方案。它包含以下核心特征: 22 | 23 | * **简单、熟悉的 API**:不需要额外的学习成本,只需要了解 React Hooks,对 Redux 用户友好。 24 | * **集成异步处理**:记录异步操作时的执行状态,简化视图中对于等待或错误的处理逻辑。 25 | * **支持组件 Class 写法**:友好的兼容策略可以让老项目享受轻量状态管理的乐趣。 26 | * **良好的 TypeScript 支持**:提供完整的 TypeScript 类型定义,在 VS Code 中能获得完整的类型检查和推断。 27 | 28 | 查看[《能力对比表》](docs/recipes.md#Comparison)了解更多细节。 29 | 30 | ## 文档 31 | 32 | - [API](./docs/api.md) 33 | - [更多技巧](./docs/recipes.md) 34 | - [从 V1 版本升级](./docs/upgrade-guidelines.md) 35 | - [常见问题](./docs/qna.md) 36 | 37 | ## 示例 38 | 39 | - [Counter](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/counter) 40 | - [Todos](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/todos) 41 | - [Class Component Support](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/classComponent) 42 | - [withModel](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/withModel) 43 | 44 | ## 快速开始 45 | 46 | ```jsx 47 | import React from 'react'; 48 | import ReactDOM from 'react-dom'; 49 | import { createStore, createModel } from '@ice/store'; 50 | 51 | const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time)); 52 | 53 | // 1️⃣ 使用模型定义你的状态 54 | const counter = createModel({ 55 | state: 0, 56 | reducers: { 57 | increment:(prevState) => prevState + 1, 58 | decrement:(prevState) => prevState - 1, 59 | }, 60 | effects: () => ({ 61 | async asyncDecrement() { 62 | await delay(1000); 63 | this.decrement(); 64 | }, 65 | }) 66 | }); 67 | 68 | const models = { 69 | counter, 70 | }; 71 | 72 | // 2️⃣ 创建 Store 73 | const store = createStore(models); 74 | 75 | 76 | // 3️⃣ 消费模型 77 | const { useModel } = store; 78 | function Counter() { 79 | const [ count, dispatchers ] = useModel('counter'); 80 | const { increment, asyncDecrement } = dispatchers; 81 | return ( 82 |
83 | {count} 84 | 85 | 86 |
87 | ); 88 | } 89 | 90 | // 4️⃣ 绑定视图 91 | const { Provider } = store; 92 | function App() { 93 | return ( 94 | 95 | 96 | 97 | ); 98 | } 99 | 100 | const rootElement = document.getElementById('root'); 101 | ReactDOM.render(, rootElement); 102 | ``` 103 | 104 | ## 安装 105 | 106 | 使用 icestore 需要 React 在 16.8.0 版本以上。 107 | 108 | ```bash 109 | npm install @ice/store --save 110 | ``` 111 | 112 | ## 灵感 113 | 114 | 创造 icestore 的灵感来自于 [rematch](https://github.com/rematch/rematch) 和 [constate](https://github.com/diegohaz/constate)。 115 | 116 | ## 参与贡献 117 | 118 | 欢迎通过 [issue](https://github.com/ice-lab/icestore/issues/new) 反馈问题。 119 | 120 | 开发: 121 | 122 | ```bash 123 | $ cd icestore/ 124 | $ npm install 125 | $ npm run test 126 | $ npm run watch 127 | 128 | $ cd examples/counter 129 | $ npm install 130 | $ npm link ../../ # link icestore 131 | $ npm link ../../node_modules/react # link react 132 | $ npm start 133 | ``` 134 | 135 | ## License 136 | 137 | [MIT](LICENSE) 138 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false 5 | require_base: no 6 | require_head: yes 7 | branches: 8 | - "master" 9 | 10 | coverage: 11 | status: 12 | project: 13 | default: 14 | threshold: 90% 15 | patch: off -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: api 3 | title: API 4 | --- 5 | 6 | `createStore` 是 @ice/store 的 API 主要入口。创建后的 Store 将提供一些 Hooks 和 API 用于访问和操作数据。 7 | 8 | `createModel` 是一个类型工具方法,无任何副作用。用它来包裹你的 model 对象,在 effects 中使用 `this` 时可获得完整的类型提示。 9 | 10 | ## createModel 11 | 12 | `createModel(modelConfig)` 13 | 14 | 该方法用于包裹 model 对象,以获得更好的类型提示。 15 | 16 | ```ts 17 | import { createModel } from '@ice/store'; 18 | 19 | type IState = { 20 | count: number, 21 | }; 22 | const state: IState = { 23 | count: 0, 24 | }; 25 | 26 | const counter = createModel({ 27 | state, 28 | reducers: { 29 | increment(state: IState, payload: number) { 30 | return state.count + payload; 31 | }, 32 | decrement(state: IState, payload: number) { 33 | return state.count - payload; 34 | }, 35 | }, 36 | effects: () => ({ 37 | async asyncDecrement(payload: number) { 38 | this.decrement(payload); 39 | }, 40 | async anotherEffect(payload: number) { 41 | this.asyncDecrement(payload); 42 | }, 43 | }), 44 | }); 45 | 46 | const models = { 47 | counter, 48 | } 49 | ``` 50 | 51 | ## createStore 52 | 53 | `createStore(models, options)` 54 | 55 | 该方法用于创建 Store。 56 | 57 | ```js 58 | import { createStore } from '@ice/store'; 59 | 60 | const { 61 | // 主要的 API 62 | Provider, 63 | useModel, 64 | getModel, 65 | withModel, 66 | 67 | // 辅助的 API 68 | useModelDispatchers, 69 | useModelEffectsState, 70 | withModelDispatchers, 71 | withModelEffectsState, 72 | getModelState, 73 | getModelDispatchers, 74 | } = createStore(models); 75 | ``` 76 | 77 | ### 参数 78 | 79 | #### models 80 | 81 | `createStore({ [string]: modelConfig });` 82 | 83 | ```js 84 | import { createStore, createModel } from '@ice/store' 85 | 86 | const count = createModel({ 87 | state: 0, 88 | }); 89 | 90 | createStore({ 91 | count 92 | }); 93 | ``` 94 | 95 | ##### state 96 | 97 | `state: any`: 必填 98 | 99 | 该 model 的初始 state。 100 | 101 | ```js 102 | import { createModel } from '@ice/store'; 103 | 104 | interface State { 105 | loading: boolean; 106 | } 107 | 108 | const model = createModel({ 109 | state: { loading: false } as State, 110 | }); 111 | ``` 112 | 113 | ##### reducers 114 | 115 | `reducers: { [string]: (state, payload) => any }` 116 | 117 | 一个改变该模型状态的函数集合。这些方法以模型的上一次 state 和一个 payload 作为入参,在方法中使用可变的方式来更新状态。 118 | 这些方法应该是仅依赖于 state 和 payload 参数来计算下一个 state 的纯函数。对于有副作用的函数,请使用 effects。 119 | 120 | 一个简单的示例: 121 | 122 | ```ts 123 | import { createModel } from '@ice/store'; 124 | 125 | interface State { 126 | title: string; 127 | done: boolean; 128 | } 129 | 130 | const todos = createModel({ 131 | state: [ 132 | { 133 | title: 'Learn typescript', 134 | done: true, 135 | }, 136 | ] as State[], 137 | reducers: { 138 | foo(state) { 139 | state.push({ title: 'Tweet about it' }); // 直接更新了数组 140 | state[1].done = true; 141 | }, 142 | }, 143 | }); 144 | ``` 145 | 146 | icestore 内部是通过调用 [immer](https://github.com/immerjs/immer) 来实现可变状态的。 147 | Immer 只支持对普通对象和数组的变化检测,所以像字符串或数字这样的类型需要返回一个新值。 例如: 148 | 149 | ```js 150 | import { createModel } from '@ice/store'; 151 | 152 | const count = createModel({ 153 | state: 0, 154 | reducers: { 155 | add(state) { 156 | state += 1; 157 | return state; 158 | }, 159 | }, 160 | }); 161 | ``` 162 | 163 | 参考 [docs/recipes](./recipes.md#可变状态的说明) 了解更多。 164 | 165 | reducer 的第二个参数即是调用时传递的参数: 166 | 167 | ```js 168 | import { createModel } from '@ice/store'; 169 | 170 | const todos = createModel({ 171 | state: [ 172 | { 173 | title: 'Learn typescript', 174 | done: true, 175 | }, 176 | ], 177 | reducers: { 178 | // 正确用法 179 | add(state, todo) { 180 | state.push(todo); 181 | }, 182 | // 错误用法 183 | add(state, title, done) { 184 | state.push({ title, done }); 185 | }, 186 | }, 187 | }); 188 | 189 | // 使用时: 190 | function Component() { 191 | const { add } = store.useModelDispatchers('todos'); 192 | function handleClick () { 193 | add({ title: 'Learn React', done: false }); // 正确用法 194 | add('Learn React', false); // 错误用法 195 | } 196 | } 197 | ``` 198 | 199 | ##### effects 200 | 201 | `effects: (dispatch) => ({ [string]: (payload, rootState) => void })` 202 | 203 | 一个可以处理该模型副作用的函数集合。这些方法以 payload 和 rootState 作为入参,适用于进行异步调用、[模型联动](recipes.md#模型联动)等场景。在 effects 内部,通过调用 `this.reducerFoo` 来更新模型状态: 204 | 205 | ```js 206 | import { createModel } from '@ice/store'; 207 | 208 | const counter = createModel({ 209 | state: 0, 210 | reducers: { 211 | decrement:(prevState) => prevState - 1, 212 | }, 213 | effects: () => ({ 214 | async asyncDecrement() { 215 | await delay(1000); // 进行一些异步操作 216 | this.decrement(); // 调用模型 reducers 内的方法来更新状态 217 | }, 218 | }), 219 | }); 220 | ``` 221 | 222 | > 注意:如果您正在使用 TypeScript ,并且配置了编译选项 `noImplicitThis: ture`,则会遇到类似 "Property 'setState' does not exist on type" 的编译错误。您可以[参考qna中的用法](qna.md)使用 `createModel` 来包裹你的 model,或者使用下面示例中的 `dispatch.model.reducer` 来避免此错误。 223 | 224 | ###### 同名处理 225 | 226 | 如果 reducers 和 effects 中的方法重名,则会在先执行 reducer.foo 后再执行 effects.foo: 227 | 228 | ```js 229 | import { createModel } from '@ice/store'; 230 | 231 | const model = createModel({ 232 | state: [], 233 | reducers: { 234 | add(state, todo) { 235 | state.push(todo); 236 | }, 237 | }, 238 | effects: (dispatch: RootDispatch) => ({ 239 | // 将会在 reducers.add 执行完成后再执行该方法 240 | add(todo) { 241 | dispatch.user.setTodos(store.getModelState('todos').length); 242 | }, 243 | }) 244 | }); 245 | ``` 246 | 247 | ###### this.setState 248 | 249 | icestore 内置提供了名为 `setState` reducer ,其作用类似于 React Class 组件中的 [setState](https://zh-hans.reactjs.org/docs/react-component.html#setstate),但仅支持一个参数且参数是对象类型。 250 | 251 | ```js 252 | this.setState(stateChange); 253 | 254 | // stateChange 会将传入的对象浅层合并到新的 state 中,例如,调整购物车商品数: 255 | this.setState({quantity: 2}); 256 | ``` 257 | 258 | setState 的 reducer 内部实现类似于: 259 | 260 | ```js 261 | const setState = (prevState, payload) => ({ 262 | ...prevState, 263 | ...payload, 264 | }); 265 | ``` 266 | 267 | 您可以通过在 reducers 中声明 `setState` 来覆盖默认的行为: 268 | 269 | ```js 270 | import { createModel } from '@ice/store'; 271 | 272 | const model = createModel({ 273 | state: { count: 0, calledCounter: 0 }, 274 | reducers: { 275 | setState: (prevState, payload) => ({ 276 | ...prevState, 277 | ...payload, 278 | calledCounter: prevState.calledCounter + 1, 279 | }) 280 | }, 281 | effects: () => ({ 282 | foo() { 283 | this.setState({ count: 1 }); 284 | } 285 | }) 286 | }) 287 | ``` 288 | 289 | ###### 模型联动 290 | 291 | 您可以通过声明 effects 函数的第一个参数 `dispatch` 来调用其他模型的方法: 292 | 293 | ```js 294 | import { createStore, createModel } from '@ice/store'; 295 | 296 | const user = createModel({ 297 | state: { 298 | foo: [], 299 | }, 300 | effects: (dispatch) => ({ 301 | like(payload, rootState) { 302 | this.doSomething(payload); // 调用 user 内的其他 effect 或 reducer 303 | // 另一种调用方式:dispatch.user.doSomething(payload); 304 | dispatch.todos.foo(payload); // 调用其他模型的 effect 或 reducer 305 | }, 306 | doSomething(payload) { 307 | // ... 308 | this.foo(payload); 309 | } 310 | }), 311 | reducers: { 312 | foo(prevState, payload) { 313 | return { 314 | ...prevState, 315 | }; 316 | }, 317 | } 318 | }); 319 | 320 | const todos = { /* ... */ }; 321 | 322 | const store = createStore({ user, todos }); 323 | ``` 324 | 325 | 参考 [docs/recipes](./recipes.md#模型联动) 了解更多。 326 | 327 | #### options 328 | 329 | - `disableImmer` (布尔值, 可选, 默认值=false) 330 | 331 | 如果您将其设置为true,那么 immer 将被禁用,这意味着您不能再在 reducers 中直接改变状态,而是必须返回新的状态。 332 | 333 | - `disableError` (布尔值, 可选, 默认值=false) 334 | 335 | 如果将此设置为true,则 “UseModelEffectsError” 和 “WithModelEffectsError” 将不可用。仅当您非常关注性能或故意抛出错误时才启用该选项。 336 | 337 | - `disableLoading` (布尔值, 可选, 默认值=false) 338 | 339 | 如果将此设置为true,则“useModelEffectsLoading”和“withModelEffectsLoading”将不可用。 340 | 341 | ### 返回值 342 | 343 | #### Provider 344 | 345 | `Provider(props: { children })` 346 | 347 | 将 store 和 React 应用进行绑定,因此可以在组件中使用 store 提供的 hooks。 348 | 349 | ```jsx 350 | import React from 'react'; 351 | import ReactDOM from 'react-dom'; 352 | import { createStore } from '@ice/store'; 353 | 354 | const { Provider } = createStore(models); 355 | ReactDOM.render( 356 | 357 | 358 | , 359 | rootEl 360 | ); 361 | ``` 362 | 363 | 允许您声明初始状态(可以用在诸如服务端渲染等场景)。 364 | 365 | ```jsx 366 | import { createStore, createModel } from '@ice/store'; 367 | 368 | const models = { 369 | todo: createModel({ state: {} }), 370 | user: createModel({ state: {}, }), 371 | }; 372 | 373 | const store = createStore(models); 374 | const { Provider } = store; 375 | 376 | const initialStates = { 377 | todo: { 378 | title: 'Foo', 379 | done: true, 380 | }, 381 | user: { 382 | name: 'Alvin', 383 | age: 18, 384 | }, 385 | }; 386 | function App() { 387 | return ( 388 | 389 | 390 | 391 | ); 392 | } 393 | ``` 394 | 395 | #### useModel 396 | 397 | `useModel(name: string): [ state, dispatchers ]` 398 | 399 | 通过该 hooks 使用模型,返回模型的状态和调度器。 400 | 401 | ```jsx 402 | import { createModel } from '@ice/store'; 403 | 404 | const counter = createModel({ 405 | state: { 406 | value: 0, 407 | }, 408 | reducers: { 409 | add: (state, payload) => { 410 | state.value = state.value + payload; 411 | }, 412 | }, 413 | }); 414 | 415 | const { useModel } = createStore({ counter }); 416 | 417 | function FunctionComponent() { 418 | const [ state, dispatchers ] = useModel('counter'); 419 | 420 | state.value; // 0 421 | 422 | dispatchers.add(1); // state.value === 1 423 | } 424 | ``` 425 | 426 | #### getModel 427 | 428 | `getModel(name: string): [ state, dispatchers ]` 429 | 430 | 通过 API 获取到最新的模型,在闭包中将非常有用。 431 | 432 | ```js 433 | import { useCallback } from 'react'; 434 | import store from '@/store'; 435 | 436 | function FunctionComponent() { 437 | const memoizedCallback = useCallback( 438 | () => { 439 | const [state] = store.getModel('foo'); 440 | doSomething(a, b, state); 441 | }, 442 | [a, b], 443 | ); 444 | } 445 | ``` 446 | 447 | #### withModel 448 | 449 | `withModel(name: string, mapModelToProps?: (model: [state, dispatchers]) => Object = (model) => ({ [name]: model }) ): (React.Component) => React.Component` 450 | 451 | 使用该 API 将模型绑定到 Class 组件上。 452 | 453 | ```jsx 454 | import { ExtractIModelFromModelConfig } from '@ice/store'; 455 | import todosModel from '@/models/todos'; 456 | import store from '@/store'; 457 | 458 | interface Props { 459 | todos: ExtractIModelFromModelConfig; // `withModel` automatically adds the name of the model as the property 460 | } 461 | 462 | class TodoList extends Component { 463 | render() { 464 | const { counter } = this.props; 465 | const [ state, dispatchers ] = counter; 466 | 467 | state.value; // 0 468 | 469 | dispatchers.add(1); 470 | } 471 | } 472 | 473 | export default withModel('counter')(TodoList); 474 | ``` 475 | 476 | 可以使用 `mapModelToProps` 设置 props 的字段名: 477 | 478 | ```tsx 479 | import { ExtractIModelFromModelConfig } from '@ice/store'; 480 | import todosModel from '@/models/todos'; 481 | import store from '@/store'; 482 | 483 | const { withModel } = store; 484 | 485 | interface Props { 486 | title: string; 487 | customKey: ExtractIModelFromModelConfig; 488 | } 489 | 490 | class TodoList extends Component { 491 | render() { 492 | const { title, customKey } = this.props; 493 | const [ state, dispatchers ] = customKey; 494 | 495 | state.field; // get state 496 | dispatchers.add({ /* ... */}); // run action 497 | } 498 | } 499 | 500 | export default withModel( 501 | 'todos', 502 | 503 | // mapModelToProps: (model: [state, dispatchers]) => Object = (model) => ({ [modelName]: model }) ) 504 | (model) => ({ 505 | customKey: model, 506 | }) 507 | )(TodoList); 508 | ``` 509 | 510 | #### useModelState 511 | 512 | `useModelState(name: string): state` 513 | 514 | 通过该 hooks 使用模型的状态并订阅其更新。 515 | 516 | ```js 517 | function FunctionComponent() { 518 | const state = useModelState('counter'); 519 | console.log(state.value); 520 | } 521 | ``` 522 | 523 | #### useModelDispatchers 524 | 525 | `useModelDispatchers(name: string): dispatchers` 526 | 527 | 通过该 hooks 使用模型的调度器,通过调度器更新模型。 528 | 529 | ```js 530 | function FunctionComponent() { 531 | const dispatchers = useModelDispatchers('counter'); 532 | dispatchers.add(1); 533 | } 534 | ``` 535 | 536 | #### useModelEffectsLoading 537 | 538 | `useModelEffectsLoading(name: string): { [actionName: string]: boolean }` 539 | 540 | 通过该 hooks 来获取模型副作用的调用状态。 541 | 542 | ```js 543 | function FunctionComponent() { 544 | const dispatchers = useModelDispatchers('counter'); 545 | const effectsLoading = useModelEffectsLoading('counter'); 546 | 547 | useEffect(() => { 548 | dispatchers.fetch(); 549 | }, []); 550 | 551 | effectsLoading.fetch; // boolean 552 | } 553 | ``` 554 | 555 | #### useModelEffectsError 556 | 557 | `useModelEffectsError(name: string): { [actionName: string]: { error: Error; value: boolean;}}` 558 | 559 | 通过该 hooks 来获取模型副作用的调用结果是否有错误。 560 | 561 | ```js 562 | function FunctionComponent() { 563 | const dispatchers = useModelDispatchers('counter'); 564 | const effectsError = useModelEffectsError('counter'); 565 | 566 | useEffect(() => { 567 | dispatchers.fetch(); 568 | }, []); 569 | 570 | effectsError.fetch.error; // Error 571 | } 572 | ``` 573 | 574 | #### withModelDispatchers 575 | 576 | `withModelDispatchers(name: string, mapModelDispatchersToProps?: (dispatchers) => Object = (dispatchers) => ({ [name]: dispatchers }) ): (React.Component) => React.Component` 577 | 578 | ```tsx 579 | import { ExtractIModelDispatchersFromModelConfig } from '@ice/store'; 580 | import todosModel from '@/models/todos'; 581 | import store from '@/store'; 582 | 583 | const { withModelDispatchers } = store; 584 | 585 | interface Props { 586 | todosDispatchers: ExtractIModelDispatchersFromModelConfig; // `withModelDispatchers` automatically adds `${modelName}Dispatchers` as the property 587 | } 588 | 589 | class TodoList extends Component { 590 | render() { 591 | const { todosDispatchers } = this.props; 592 | 593 | todosDispatchers.add({ /* ... */}); // run action 594 | } 595 | } 596 | 597 | export default withModelDispatchers('todos')(TodoList); 598 | ``` 599 | 600 | 你可以使用 `mapModelDispatchersToProps` 来设置 props 的字段名,用法同 `mapModelToProps`。 601 | 602 | #### withModelEffectsLoading 603 | 604 | `withModelEffectsLoading(name: string, mapModelEffectsLoadingToProps?: (effectsLoading) => Object = (effectsLoading) => ({ [name]: effectsLoading }) ): (React.Component) => React.Component` 605 | 606 | ```tsx 607 | import { ExtractIModelEffectsLoadingFromModelConfig } from '@ice/store'; 608 | import todosModel from '@/models/todos'; 609 | import store from '@/store'; 610 | 611 | const { withModelEffectsLoading } = store; 612 | 613 | interface Props { 614 | todosEffectsLoading: ExtractIModelEffectsLoadingFromModelConfig; // `todosEffectsLoading` automatically adds `${modelName}EffectsLoading` as the property 615 | } 616 | 617 | class TodoList extends Component { 618 | render() { 619 | const { todosEffectsLoading } = this.props; 620 | 621 | todosEffectsLoading.add; 622 | } 623 | } 624 | 625 | export default withModelEffectsLoading('todos')(TodoList); 626 | ``` 627 | 628 | 可以使用 `mapModelEffectsLoadingToProps` 参数来设置 props 的字段名,方式与 `mapModelToProps` 一致。 629 | 630 | #### withModelEffectsError 631 | 632 | `withModelEffectsError(name: string, mapModelEffectsErrorToProps?: (effectsError) => Object = (effectsError) => ({ [name]: effectsError }) ): (React.Component) => React.Component` 633 | 634 | ```tsx 635 | import { ExtractIModelEffectsErrorFromModelConfig } from '@ice/store'; 636 | import todosModel from '@/models/todos'; 637 | import store from '@/store'; 638 | 639 | const { withModelEffectsError } = store; 640 | 641 | interface Props { 642 | todosEffectsError: ExtractIModelEffectsErrorFromModelConfig; // `todosEffectsError` automatically adds `${modelName}EffectsError` as the property 643 | } 644 | 645 | class TodoList extends Component { 646 | render() { 647 | const { todosEffectsError } = this.props; 648 | 649 | todosEffectsError.add; 650 | } 651 | } 652 | 653 | export default withModelEffectsError('todos')(TodoList); 654 | ``` 655 | 656 | 可以使用 `mapModelEffectsErrorToProps` 来设置 props 的字段名,方式与 `mapModelToProps` 一致。 657 | 658 | #### getModelState 659 | 660 | `getModelState(name: string): state` 661 | 662 | 通过该 API 获取模型的最新状态。 663 | 664 | ```js 665 | import { useCallback } from 'react'; 666 | import store from '@/store'; 667 | 668 | function FunctionComponent() { 669 | const memoizedCallback = useCallback( 670 | () => { 671 | const state = store.getModelState('foo'); 672 | something(a, state); 673 | }, 674 | [a, b], 675 | ); 676 | } 677 | ``` 678 | 679 | #### getModelDispatchers 680 | 681 | `getModelDispatchers(name: string): dispatchers` 682 | 683 | 通过该 API 来获取模型的调度器。 684 | 685 | ```js 686 | import { useCallback } from 'react'; 687 | import store from '@/store'; 688 | 689 | function FunctionComponent() { 690 | const memoizedCallback = useCallback( 691 | () => { 692 | const dispatchers = store.getModelDispatchers('foo'); 693 | dispatchers.foo(a, b); 694 | }, 695 | [a, b], 696 | ); 697 | } 698 | ``` 699 | 700 | ## withModel 701 | 702 | `withModel(model, mapModelToProps?, options?)(ReactFunctionComponent)` 703 | 704 | 该方法用于在组件中快速使用 Model。 705 | 706 | ```js 707 | import { withModel } from '@ice/store'; 708 | import model from './model'; 709 | 710 | function Todos({ model }) { 711 | const { 712 | useState, 713 | useDispatchers, 714 | useEffectsState, 715 | getState, 716 | getDispatchers, 717 | } = model; 718 | const [ state, dispatchers ] = useValue(); 719 | } 720 | 721 | export default withModel(model)(Todos); 722 | ``` 723 | 724 | ### 参数 725 | 726 | #### modelConfig 727 | 728 | 与 createStore 方法中的 modelConfig 一致。 729 | 730 | #### mapModelToProps 731 | 732 | `mapModelToProps = (model) => ({ model })` 733 | 734 | 使用该函数来自定义映射到组件中的值,使用示例: 735 | 736 | ```js 737 | import { withModel } from '@ice/store'; 738 | import model from './model'; 739 | 740 | function Todos({ todo }) { 741 | const [ state, dispatchers ] = todo.useValue(); 742 | } 743 | 744 | export default withModel(model, function(model) { 745 | return { todo: model }; 746 | })(Todos); 747 | ``` 748 | 749 | #### options 750 | 751 | 与 createStore 方法中的 options 一致。 752 | 753 | ### 返回值 754 | 755 | - useValue 756 | - useState 757 | - useDispathers 758 | - useEffectsState 759 | - getValue 760 | - getState 761 | - getDispatchers 762 | - withValue 763 | - withDispatchers 764 | - withModelEffectsState 765 | 766 | 其用法参考 createStore 的返回值。 767 | -------------------------------------------------------------------------------- /docs/qna.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: qna 3 | title: 常见问题 4 | --- 5 | 6 | ## 在 TypeScript 项目中 Model 内无法使用 `this` 7 | 8 | ![image](https://user-images.githubusercontent.com/4392234/85498836-09024900-b613-11ea-9150-8287b4455e92.png) 9 | 10 | ### 原因 11 | 12 | `tsconfig.json` 里面设置了: 13 | 14 | ```json 15 | { 16 | "compilerOptions": { 17 | "strict": true, 18 | 19 | // 或 20 | "noImplicitThis": true, 21 | } 22 | } 23 | ``` 24 | 25 | ### 解决方法 26 | 27 | > 更推荐使用方法一,可以获得完善的 TS 类型提示 28 | 29 | 方法一:使用 `createModel` 工具方法来包裹你的 model 对象 30 | 31 | ```diff 32 | + import { createModel } from '@ice/store'; 33 | 34 | - const counter = { 35 | + const counter = createModel({ 36 | state: 0, 37 | reducers: { 38 | increment: (prevState) => prevState + 1, 39 | decrement: (prevState) => prevState - 1, 40 | }, 41 | effects: () => ({ 42 | async asyncDecrement() { 43 | await delay(1000); 44 | this.decrement(); 45 | }, 46 | }), 47 | - }; 48 | + }); 49 | ``` 50 | 51 | ![image](https://user-images.githubusercontent.com/42671099/163668927-2a30ec43-7c49-4973-ae15-1035a0386ca7.png) 52 | 53 | 方法二:使用 `dispatch` 54 | 55 | ```diff 56 | const counter = { 57 | state: 0, 58 | reducers: { 59 | increment: (prevState) => prevState + 1, 60 | decrement: (prevState) => prevState - 1, 61 | }, 62 | - effects: () => ({ 63 | + effects: (dispatch) => ({ 64 | async asyncDecrement() { 65 | await delay(1000); 66 | - // this.decrement(); 67 | + dispatch.counter.decrement(); 68 | }, 69 | }), 70 | }; 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/recipes.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: recipes 3 | title: 更多技巧 4 | --- 5 | 6 | ## 类型增强 7 | 8 | 在 `store.ts` 中增加如下代码,在书写 model 时可以增强 effects 中 dispatch 和 rootState 的类型提示。 9 | 10 | ```tsx 11 | declare module '@ice/store' { 12 | interface IcestoreModels { 13 | // 此处替换为项目中真实的 model 名称 14 | counter: typeof counter; 15 | user: typeof user; 16 | } 17 | } 18 | 19 | ``` 20 | ### 效果 21 | 22 | ![image](https://user-images.githubusercontent.com/42671099/173331507-df062d1a-ec42-4c71-b931-1f017bef5ffa.png) 23 | 24 | ![image](https://user-images.githubusercontent.com/42671099/173331558-c9a41289-d3d2-4d76-a30c-8f4cdda857e2.png) 25 | 26 | ## 模型联动 27 | 28 | 模型联动是一个非常常见的场景,可以实现在一个模型中触发另一个模型状态的变更。 29 | 30 | ### 示例 31 | 32 | 您有一个用户模型,记录了用户拥有多少个任务;还有一个任务模型,记录了任务的列表详情。每当添加任务到列表时,都需要更新用户拥有的任务数。 33 | 34 | ```tsx 35 | import { createModel } from '@ice/store'; 36 | 37 | // src/models/user 38 | export default createModel({ 39 | state: { 40 | name: '', 41 | tasks: 0, 42 | }, 43 | effects: () => ({ 44 | async refresh() { 45 | const data = await fetch('/user'); 46 | this.setState(data); 47 | }, 48 | }), 49 | }); 50 | 51 | // src/models/tasks 52 | import { createModel } from '@ice/store'; 53 | 54 | export default createModel({ 55 | state: [], 56 | effects: (dispatch) => ({ 57 | async refresh() { 58 | const data = await fetch('/tasks'); 59 | this.setState(data); 60 | }, 61 | async add(task) { 62 | await fetch('/tasks/add', task); 63 | 64 | // 调度用户模型从服务端获取最新数据 65 | await dispatch.user.refresh(); 66 | 67 | // 调度任务模型从服务端获取最新数据 68 | await this.refresh(); 69 | }, 70 | }), 71 | }); 72 | 73 | // src/store 74 | import { createStore } from '@ice/store'; 75 | import task from './model/task'; 76 | import user from './model/user'; 77 | 78 | export default createStore({ 79 | task, 80 | user, 81 | }); 82 | ``` 83 | 84 | ### 注意循环调用问题 85 | 86 | 模型间允许相互调用,需注意循环调用的问题。例如,模型 A 中的 a 方法调用了 模型 B 中的 b 方法,模型 B 中的 b 方法又调用模型 A 中的 a 方法,就会形成死循环。 87 | 88 | 如果是多个模型间进行相互调用,死循环问题的出现概率就会提升。 89 | 90 | ## 只调用方法而不订阅更新 91 | 92 | 在某些场景下,您可能只希望调用模型方法来更新状态而不订阅模型状态的更新。 例如「快速开始」示例中的 Button 组件,您没有在组件中消费模型的状态,因此可能不期望模型状态的变化触发组件的重新渲染。 这时候您可以使用 useModelDispatchers API,看下面的示例比较: 93 | 94 | ```js 95 | import store from '@/store'; 96 | 97 | const { useModelDispatchers } = store; 98 | function Button() { 99 | const [, dispatchers ] = useModel('counter'); // 这里会订阅模型状态的更新 100 | const { increment } = dispatchers; 101 | return ( 102 | 103 | ); 104 | } 105 | 106 | function Button() { 107 | const { increment } = useModelDispatchers('counter'); // 这里不会订阅模型状态的更新 108 | return ( 109 | 110 | ); 111 | } 112 | ``` 113 | 114 | ## 获取模型最新状态 115 | 116 | 在某些场景下,您可能需要获取到模型的最新状态。 117 | 118 | ### 在组件中 119 | 120 | ```js 121 | import store from '@/store'; 122 | 123 | function Logger({ foo }) { 124 | // case 1 只使用状态而不订阅更新(性能优化的手段) 125 | function doSomeThing() { 126 | const counter = store.getModelState('counter'); 127 | alert(counter); 128 | }; 129 | 130 | 131 | // case 2 在闭包中获取最新状态 132 | const doOtherThing = useCallback( 133 | (payload) => { 134 | const counter = store.getModelState('counter'); 135 | alert(counter + foo); 136 | }, 137 | [foo] 138 | ); 139 | 140 | return ( 141 |
142 |
145 | ); 146 | } 147 | ``` 148 | 149 | ### 在模型中 150 | 151 | ```js 152 | import { createModel } from '@ice/store'; 153 | import store from '@/store'; 154 | 155 | const user = createModel({ 156 | effects: dispatch => ({ 157 | async asyncAdd(payload, state) { 158 | dispatch.todos.addTodo(payload); // 调用其他模型的方法更新其状态 159 | const todos = store.getModelState('todos'); // 获取更新后的模型最新状态 160 | } 161 | }) 162 | }) 163 | ``` 164 | 165 | ## 模型副作用的执行状态 166 | 167 | @ice/store 内部集成了对于异步副作用的状态记录,方便您在不增加额外的状态的前提下访问异步副作用的执行状态(loading 与 error),从而使状态渲染的处理逻辑更加简洁。 168 | 169 | ### 示例 170 | 171 | ```js 172 | import { useModelDispatchers } from './store'; 173 | 174 | function FunctionComponent() { 175 | const dispatchers = useModelDispatchers('name'); 176 | const effectsState = useModelEffectsState('name'); 177 | 178 | useEffect(() => { 179 | dispatchers.fetch(); 180 | }, []); 181 | 182 | effectsState.fetch.isLoading; // 是否在调用中 183 | effectsState.fetch.error; // 调用结果是否有错误 184 | } 185 | ``` 186 | 187 | ## 在 Class 组件中使用模型 188 | 189 | 您可以在 Class 组件中使用模型,只需要调用 `withModel()` 方法将模型绑定到 React 组件中。 190 | 191 | ### 基础示例 192 | 193 | ```tsx 194 | import { ExtractIModelFromModelConfig } from '@ice/store'; 195 | import todosModel from '@/models/todos'; 196 | import store from '@/store'; 197 | 198 | const { withModel } = store; 199 | 200 | interface MapModelToProp { 201 | todos: ExtractIModelFromModelConfig; // `withModel` 自动添加的 props 字段用于访问模型 202 | } 203 | 204 | interface Props extends MapModelToProp { 205 | title: string; // 自定义的 props 206 | } 207 | 208 | class TodoList extends Component { 209 | render() { 210 | const { title, todos } = this.props; 211 | const [ state, dispatchers ] = todos; 212 | 213 | state.field; // 获取状态 214 | dispatchers.add({ /* ... */}); // 调度模型的变更操作 215 | } 216 | } 217 | 218 | export default withModel('todos')(TodoList); 219 | ``` 220 | 221 | ### 使用多个模型 222 | 223 | ```tsx 224 | import { ExtractIModelFromModelConfig } from '@ice/store'; 225 | import todosModel from '@/models/todos'; 226 | import userModel from '@/models/user'; 227 | import store from '@/store'; 228 | 229 | const { withModel } = store; 230 | 231 | interface Props { 232 | todos: ExtractIModelFromModelConfig; 233 | user: ExtractIModelFromModelConfig; 234 | } 235 | 236 | class TodoList extends Component { 237 | render() { 238 | const { todos, user } = this.props; 239 | const [ todoState, todoDispatchers ] = todos; 240 | const [ userState, userDispatchers ] = user; 241 | } 242 | } 243 | 244 | export default withModel('user')( 245 | withModel('todos')(TodoList) 246 | ); 247 | 248 | // 可以通过组合的方式进行函数调用: 249 | import compose from 'lodash/fp/compose'; 250 | export default compose(withModel('user'), withModel('todos'))(TodoList); 251 | ``` 252 | 253 | ### withModelDispatchers & withModelEffectsState 254 | 255 | 您可以使用 `withModelDispatchers` 用于使用模型的调度器而不订阅模型的更新,`withModelEffectsState` 的 API 签名与前者一致。 256 | 257 | 查看 [docs/api](./api.zh-CN.md) 了解其使用方式。 258 | 259 | ## 可变状态的说明 260 | 261 | icestore 默认为 reducer 提供了状态可变的操作方式。 262 | 263 | ### 不要解构参数 264 | 265 | icestore 内部使用 [immer](https://github.com/immerjs/immer) 来实现可变状态的操作 API。immer 使用代理(Proxy)来跟踪我们的变化,然后将它们转换为新的更新。因此,如果对提供的状态进行解构,则会脱离代理,在此之后,将不会检测到它的任何更新。 266 | 267 | 下面是几个错误的示范: 268 | 269 | ```js 270 | import { createModel } from '@ice/store'; 271 | 272 | const model = createModel({ 273 | state: { 274 | items: [], 275 | }, 276 | reducers: { 277 | addTodo({ items }, payload) { 278 | items.push(payload); 279 | }, 280 | 281 | // or 282 | 283 | addTodo(state, payload) => { 284 | const { items } = state; 285 | items.push(payload); 286 | } 287 | } 288 | }) 289 | ``` 290 | 291 | ### 直接更新状态 292 | 293 | 默认情况下,我们使用 immer 提供可变状态的操作。但这是完全可选的,您可以像下面这样操作,返回新的状态。 294 | 295 | ```js 296 | import { createModel } from '@ice/store'; 297 | 298 | const model = createModel({ 299 | state: [], 300 | reducers: { 301 | addTodo((prevState, payload) { 302 | // 👇 new immutable state returned 303 | return [...prevState, payload]; 304 | }) 305 | } 306 | }) 307 | ``` 308 | 309 | 如果您喜欢这种方式,可以通过 createStore 的 disableImmer 选项来禁用 immer。 310 | 311 | ```js 312 | import { createStore } from '@ice/store'; 313 | 314 | const store = createStore(models, { 315 | disableImmer: true; // 👈 通过该配置禁用 immer 316 | }); 317 | ``` 318 | 319 | ## 项目的目录组织 320 | 321 | 对于大多数中小型项目,建议集中管理模型,例如在 “src/models/” 目录中存放项目的所有模型: 322 | 323 | ```bash 324 | ├── src/ 325 | │ ├── components/ 326 | │ │ └── NotFound/ 327 | │ ├── pages/ 328 | │ │ └── Home 329 | │ ├── models/ 330 | │ │ ├── modelA.js 331 | │ │ ├── modelB.js 332 | │ │ ├── modelC.js 333 | │ │ └── index.js 334 | │ └── store.js 335 | ``` 336 | 337 | 如果项目相对较大,可以按照页面来管理模型。但是,在这种情况下,应该避免跨页面使用模型。 338 | 339 | ## Devtools 340 | 341 | icestore 默认支持 [Redux Devtools](https://github.com/zalmoxisus/redux-devtools-extension),不需要额外的配置。 342 | 343 | ```js 344 | import { createStore } from '@ice/store'; 345 | 346 | const models = { counter: {} }; 347 | createStore(models); // devtools up and running 348 | ``` 349 | 350 | 可以通过额外的参数添加 Redux Devtools 的[配置选项](https://github.com/zalmoxisus/redux-devtools-extension/blob/master/docs/API/Arguments.md)。 351 | 352 | ```js 353 | import { createStore } from '@ice/store'; 354 | 355 | const models = { counter: {} }; 356 | createStore( 357 | models, 358 | { 359 | redux: { 360 | devtoolOptions: options, 361 | }, 362 | } 363 | ); 364 | ``` 365 | 366 | ## 能力对比表 367 | 368 | - O: 支持 369 | - X: 不支持 370 | - +: 需要额外地进行能力扩展 371 | 372 | | 功能/库 | redux | constate | zustand | react-tracked | icestore | 373 | | :--------| :--------| :-------- | :-------- | :-------- | :-------- | 374 | | 框架 | Any | React | React | React | React | 375 | | 简单性 | ★★ | ★★★★ | ★★★ | ★★★ | ★★★★ | 376 | | 更少的模板代码 | ★ | ★★ | ★★★ | ★★★ | ★★★★ | 377 | | 可配置性 | ★★ | ★★★ | ★★★ | ★★★ | ★★★★★ | 378 | | 共享状态 | O | O | O | O | O | 379 | | 复用状态 | O | O | O | O | O | 380 | | 状态联动 | + | + | + | + | O | 381 | | Class 组件支持 | O | + | + | + | O | 382 | | Function 组件支持 | O | O | O | O | O | 383 | | 异步更新的状态 | + | X | X | X | O | 384 | | SSR | O | O | X | O | O | 385 | | 持久化 | + | X | X | X | + | 386 | | 懒加载模型 | + | + | + | + | O | 387 | | 中心化 | + | X | X | X | O | 388 | | 中间件或插件机制 | O | X | O | X | O | 389 | | 开发者工具 | O | X | O | X | O | 390 | -------------------------------------------------------------------------------- /docs/upgrade-guidelines.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: upgrade-guidelines 3 | title: 从老版本升级 4 | --- 5 | 6 | ## 从 V1 升级到 V2 7 | 8 | V2 版本主要变化: 9 | 10 | - Pure ESM Packages 11 | - 移除 V1 版本中已被标记为「已过期」的 API 12 | 13 | 如果你的代码在 icestore V1 下没有任何 Warning,则可以直接升级到 V2 版本。具体包含以下 Warning: 14 | 15 | - `useModelActionsState` API has been detected, please use `useModelEffectsState` instead. 16 | - `useModelActions` API has been detected, please use `useModelDispatchers` instead. 17 | - `withModelActions` API has been detected, please use `withModelDispatchers` instead. 18 | - `withModelActionsState` API has been detected, please use `withModelEffectsState` instead. 19 | - Model: Defining effects as objects has been detected, please use \`{ effects: () => ({ effectName: () => {} }) }\` instead. 20 | - Model(${name}): The actions field has been detected, please use \`reducers\` and \`effects\` instead. 21 | 22 | 针对这些 Warning,可以参考 [icestore V1 文档](https://github.com/ice-lab/icestore/blob/stable/1.x/docs/upgrade-guidelines.zh-CN.md)进行升级。 -------------------------------------------------------------------------------- /examples/classComponent/.gitgnore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /examples/classComponent/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "disableRuntime": true, 3 | "entry": "src/index" 4 | } 5 | -------------------------------------------------------------------------------- /examples/classComponent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ice/store": "^2.0.0", 7 | "react-dom": "^16.8.6" 8 | }, 9 | "devDependencies": { 10 | "@types/jest": "^24.0.0", 11 | "@types/node": "^12.0.0", 12 | "@types/react": "^16.9.0", 13 | "@types/react-dom": "^16.9.0", 14 | "ice.js": "^1.18.1", 15 | "typescript": "^3.7.5", 16 | "utility-types": "^3.10.0" 17 | }, 18 | "scripts": { 19 | "start": "icejs start", 20 | "build": "icejs build" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/classComponent/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Counter App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/Product/Class.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Assign } from 'utility-types'; 3 | import { withModel, ExtractIModelAPIsFromModelConfig, ExtractIModelFromModelConfig } from '@ice/store'; 4 | import Product from './Product'; 5 | 6 | import model from './model'; 7 | 8 | interface CustomProp { 9 | title: string; 10 | } 11 | 12 | interface MapModelToComponentProp { 13 | model: ExtractIModelFromModelConfig; 14 | } 15 | 16 | type ComponentProps = Assign; 17 | 18 | class Component extends React.Component { 19 | render() { 20 | const { model, title } = this.props; 21 | const [state] = model; 22 | return ( 23 | 28 | ); 29 | } 30 | } 31 | 32 | interface MapModelToProp { 33 | model: ExtractIModelAPIsFromModelConfig; 34 | } 35 | 36 | type Props = Assign; 37 | 38 | export default withModel(model)(({ model, ...otherProps }) => { 39 | const ComponentWithModel = model.withValue()(Component); 40 | return ; 41 | }); 42 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/Product/Function.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Assign } from 'utility-types'; 3 | import { withModel, ExtractIModelAPIsFromModelConfig } from '@ice/store'; 4 | import Product from './Product'; 5 | import model from './model'; 6 | 7 | interface MapModelToProp { 8 | model: ExtractIModelAPIsFromModelConfig; 9 | } 10 | 11 | interface CustomProp { 12 | title: string; 13 | } 14 | 15 | type Props = Assign; 16 | 17 | function Component({ model, title }: Props) { 18 | const [product] = model.useValue(); 19 | return ( 20 | 25 | ); 26 | } 27 | 28 | export default withModel(model)(Component); 29 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/Product/Product.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ({ productTitle, title, type }) { 4 | return ( 5 |
6 |
7 |
8 | Component Type is: {type} 9 |
10 |

{title}: {productTitle}

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/Product/index.ts: -------------------------------------------------------------------------------- 1 | // import Component from './Function'; 2 | import Component from './Class'; 3 | 4 | export default Component; 5 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/Product/model.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | state: { 3 | title: 'foo', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/User/Class.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Assign } from 'utility-types'; 3 | import { ExtractIModelFromModelConfig } from '@ice/store'; 4 | import store from '../../store'; 5 | import User from './User'; 6 | import userModel from '../../models/user'; 7 | 8 | const { withModel } = store; 9 | 10 | interface PropsWithModel { 11 | user: ExtractIModelFromModelConfig; 12 | } 13 | 14 | interface CustomProp { 15 | title: string; 16 | } 17 | 18 | type Props = Assign; 19 | 20 | class Component extends React.Component { 21 | render() { 22 | const { title, user } = this.props; 23 | const [state] = user; 24 | const { name } = state; 25 | return User({ 26 | name, 27 | title, 28 | type: 'Class', 29 | }); 30 | } 31 | } 32 | 33 | export default withModel('user')( 34 | Component, 35 | ); 36 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/User/Function.tsx: -------------------------------------------------------------------------------- 1 | import store from '../../store'; 2 | import User from './User'; 3 | 4 | const { useModel } = store; 5 | 6 | export default function ({ title }) { 7 | const [state] = useModel('user'); 8 | return User( 9 | { 10 | type: 'Function', 11 | name: state.name, 12 | title, 13 | }, 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/User/User.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ({ name, title, type }) { 4 | return ( 5 |
6 |
7 |
8 | Component Type is: {type} 9 |
10 |

{title}: {name}

11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/classComponent/src/components/User/index.ts: -------------------------------------------------------------------------------- 1 | // import Component from './Function'; 2 | import Component from './Class'; 3 | 4 | export default Component; 5 | -------------------------------------------------------------------------------- /examples/classComponent/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import store from './store'; 4 | import User from './components/User'; 5 | import Product from './components/Product'; 6 | 7 | const { Provider } = store; 8 | 9 | function App() { 10 | return ( 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | const rootElement = document.getElementById('root'); 19 | ReactDOM.render(, rootElement); 20 | -------------------------------------------------------------------------------- /examples/classComponent/src/models/index.ts: -------------------------------------------------------------------------------- 1 | import { Models } from '@ice/store'; 2 | import user from './user'; 3 | 4 | const rootModels: RootModels = { user }; 5 | 6 | // add interface to avoid recursive type checking 7 | export interface RootModels extends Models { 8 | user: typeof user; 9 | } 10 | 11 | export default rootModels; 12 | -------------------------------------------------------------------------------- /examples/classComponent/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import { createModel } from '@ice/store'; 2 | 3 | export default createModel({ 4 | state: { 5 | name: 'Icestore', 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/classComponent/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | -------------------------------------------------------------------------------- /examples/classComponent/src/store.ts: -------------------------------------------------------------------------------- 1 | 2 | import { createStore, IcestoreRootState, IcestoreDispatch } from '@ice/store'; 3 | import models from './models'; 4 | 5 | const store = createStore(models); 6 | 7 | export default store; 8 | export type Models = typeof models; 9 | export type Store = typeof store; 10 | export type RootDispatch = IcestoreDispatch; 11 | export type RootState = IcestoreRootState; 12 | -------------------------------------------------------------------------------- /examples/classComponent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/counter/.gitgnore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /examples/counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "counter", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ice/store": "^2.0.0", 7 | "react-app-polyfill": "^1.0.6", 8 | "react-dom": "^16.8.6" 9 | }, 10 | "devDependencies": { 11 | "@types/jest": "^24.0.0", 12 | "@types/node": "^12.0.0", 13 | "@types/react": "^16.9.0", 14 | "@types/react-dom": "^16.9.0", 15 | "react-scripts": "^3.4.0", 16 | "typescript": "^3.7.5" 17 | }, 18 | "scripts": { 19 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 20 | "build": "react-scripts build" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all", 27 | "ie 11" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version", 33 | "ie 11" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/counter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Counter App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/counter/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, createModel } from '@ice/store'; 4 | import 'react-app-polyfill/ie11'; 5 | import 'react-app-polyfill/stable'; 6 | 7 | const delay = (time: number) => new Promise((resolve) => setTimeout(() => resolve(), time)); 8 | 9 | // 1️⃣ Use createModel function to create a model to define your store 10 | const counter = createModel({ 11 | state: { 12 | count: 0, 13 | }, 14 | reducers: { 15 | increment: (prevState) => { 16 | prevState.count++; 17 | }, 18 | decrement: (prevState, payload: number) => ({ count: prevState.count - payload }), 19 | }, 20 | effects: (dispatch) => ({ 21 | async asyncDecrement(payload: number) { 22 | await delay(1000); 23 | this.decrement(payload || 1); 24 | dispatch.counter.decrement(1); 25 | }, 26 | async anotherEffect() { 27 | this.asyncDecrement(2); 28 | }, 29 | }), 30 | }); 31 | 32 | const models = { 33 | counter, 34 | }; 35 | 36 | declare module '@ice/store'{ 37 | interface IcestoreModels { 38 | counter: typeof counter; 39 | } 40 | } 41 | 42 | // 2️⃣ Create the store 43 | const store = createStore(models); 44 | 45 | // 3️⃣ Consume model 46 | const { useModel } = store; 47 | function Counter() { 48 | const [{ count }, dispatchers] = useModel('counter'); 49 | const { increment, asyncDecrement } = dispatchers; 50 | return ( 51 |
52 | {count} 53 | 54 | 55 |
56 | ); 57 | } 58 | 59 | // 4️⃣ Wrap your components with Provider 60 | const { Provider } = store; 61 | function App() { 62 | return ( 63 | 64 | 65 | 66 | ); 67 | } 68 | 69 | const rootElement = document.getElementById('root'); 70 | ReactDOM.render(, rootElement); -------------------------------------------------------------------------------- /examples/counter/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | -------------------------------------------------------------------------------- /examples/counter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react", 21 | "noImplicitThis": true 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/migration-redux-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icestore-migration-part-1", 3 | "version": "1.0.0", 4 | "description": "Using icestore with Redux only.", 5 | "private": true, 6 | "dependencies": { 7 | "@ice/store": "^2.0.0", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-redux": "^7.2.0" 11 | }, 12 | "devDependencies": { 13 | "react-scripts": "^3.4.0" 14 | }, 15 | "scripts": { 16 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 17 | "build": "react-scripts build" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/migration-redux-1/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | redux Migration: Part 1 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/migration-redux-1/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { incrementSharks } from './reducers/sharks'; 4 | import { incrementDolphins } from './reducers/dolphins'; 5 | 6 | const Count = props => ( 7 |
8 |
9 |
10 |

Sharks

11 |

{props.sharks}

12 | 13 |
14 |
15 |

Dolphins

16 |

{props.dolphins}

17 | 18 |
19 |
20 |

Using Redux

21 |
22 | ); 23 | 24 | const mapState = state => ({ 25 | sharks: state.sharks, 26 | dolphins: state.dolphins, 27 | }); 28 | 29 | const mapDispatch = dispatch => ({ 30 | incrementSharks: () => dispatch(incrementSharks(1)), 31 | incrementDolphins: () => dispatch(incrementDolphins(1)), 32 | }); 33 | 34 | export default connect( 35 | mapState, 36 | mapDispatch, 37 | )(Count); 38 | -------------------------------------------------------------------------------- /examples/migration-redux-1/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore } from '@ice/store'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import sharks from './reducers/sharks'; 7 | import dolphins from './reducers/dolphins'; 8 | import App from './App'; 9 | 10 | const store = createStore( 11 | {}, 12 | { 13 | redux: { 14 | reducers: { 15 | sharks, 16 | dolphins, 17 | }, 18 | }, 19 | }, 20 | ); 21 | 22 | const Root = () => ( 23 | 24 | 25 | 26 | ); 27 | 28 | ReactDOM.render(, document.querySelector('#root')); 29 | -------------------------------------------------------------------------------- /examples/migration-redux-1/src/reducers/dolphins.js: -------------------------------------------------------------------------------- 1 | const INCREMENT = 'dolphins/increment'; 2 | 3 | export const incrementDolphins = (payload) => ({ 4 | type: INCREMENT, 5 | payload, 6 | }); 7 | 8 | export default (state = 0, action) => { 9 | switch (action.type) { 10 | case INCREMENT: 11 | return state + action.payload; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /examples/migration-redux-1/src/reducers/sharks.js: -------------------------------------------------------------------------------- 1 | const INCREMENT = 'sharks/increment'; 2 | 3 | export const incrementSharks = (payload) => ({ 4 | type: INCREMENT, 5 | payload, 6 | }); 7 | 8 | export default (state = 0, action) => { 9 | switch (action.type) { 10 | case INCREMENT: 11 | return state + action.payload; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /examples/migration-redux-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icestore-migration-part-2", 3 | "version": "1.0.0", 4 | "description": "Using icestore with Redux & icestore.", 5 | "private": true, 6 | "dependencies": { 7 | "@ice/store": "^2.0.0", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-redux": "^7.2.0" 11 | }, 12 | "devDependencies": { 13 | "react-scripts": "^3.4.0" 14 | }, 15 | "scripts": { 16 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 17 | "build": "react-scripts build" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/migration-redux-2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | redux Migration: Part 1 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/migration-redux-2/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { incrementDolphins } from './reducers/dolphins'; 4 | 5 | const Count = props => ( 6 |
7 |
8 |
9 |

Sharks

10 |

{props.sharks}

11 | 12 |
13 |
14 |

Dolphins

15 |

{props.dolphins}

16 | 17 |
18 |
19 |

Mixing Redux & icestore

20 |
21 | ); 22 | 23 | const mapState = state => ({ 24 | sharks: state.sharks, 25 | dolphins: state.dolphins, 26 | }); 27 | 28 | const mapDispatch = dispatch => ({ 29 | incrementSharks: () => dispatch.sharks.increment(1), 30 | incrementDolphins: () => dispatch(incrementDolphins(1)), 31 | }); 32 | 33 | export default connect( 34 | mapState, 35 | mapDispatch, 36 | )(Count); 37 | -------------------------------------------------------------------------------- /examples/migration-redux-2/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore } from '@ice/store'; 4 | import { Provider } from 'react-redux'; 5 | 6 | import sharks from './models/sharks'; 7 | import dolphins from './reducers/dolphins'; 8 | import App from './App'; 9 | 10 | const store = createStore( 11 | { sharks }, 12 | { 13 | redux: { 14 | reducers: { 15 | dolphins, 16 | }, 17 | }, 18 | }, 19 | ); 20 | 21 | const Root = () => ( 22 | 23 | 24 | 25 | ); 26 | 27 | ReactDOM.render(, document.querySelector('#root')); 28 | -------------------------------------------------------------------------------- /examples/migration-redux-2/src/models/sharks.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: 0, 3 | reducers: { 4 | increment: (state, payload) => state + payload, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/migration-redux-2/src/reducers/dolphins.js: -------------------------------------------------------------------------------- 1 | const INCREMENT = 'dolphins/increment'; 2 | 3 | export const incrementDolphins = (payload) => ({ 4 | type: INCREMENT, 5 | payload, 6 | }); 7 | 8 | export default (state = 0, action) => { 9 | switch (action.type) { 10 | case INCREMENT: 11 | return state + action.payload; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /examples/migration-redux-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icestore-migration-part-3", 3 | "version": "1.0.0", 4 | "description": "Using icestore with react-redux only.", 5 | "private": true, 6 | "dependencies": { 7 | "@ice/store": "^2.0.0", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-redux": "^7.2.0" 11 | }, 12 | "devDependencies": { 13 | "react-scripts": "^3.4.0" 14 | }, 15 | "scripts": { 16 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 17 | "build": "react-scripts build" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/migration-redux-3/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | redux Migration: Part 1 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/migration-redux-3/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect, useSelector, useDispatch } from './redux'; 3 | import store from './store'; 4 | 5 | const Count = props => { 6 | const sharks = useSelector(state => state.sharks); 7 | const dispatch = useDispatch(); 8 | 9 | return ( 10 |
11 |
12 |
13 |

Sharks

14 |

{sharks}

15 | 16 |
17 |
18 |

Dolphins

19 |

{props.dolphins}

20 | 21 |
22 |
23 |

Using react-redux

24 |
25 | ); 26 | }; 27 | 28 | const mapState = state => ({ 29 | dolphins: state.dolphins, 30 | }); 31 | 32 | const mapDispatch = dispatch => ({ 33 | incrementDolphins: () => dispatch.dolphins.increment(1), 34 | }); 35 | 36 | export default connect( 37 | mapState, 38 | mapDispatch, 39 | undefined, 40 | { context: store.context }, 41 | )(Count); 42 | -------------------------------------------------------------------------------- /examples/migration-redux-3/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import store from './store'; 4 | 5 | import App from './App'; 6 | 7 | const Root = () => ( 8 | 9 | 10 | 11 | ); 12 | 13 | ReactDOM.render(, document.querySelector('#root')); 14 | -------------------------------------------------------------------------------- /examples/migration-redux-3/src/models/dolphins.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: 0, 3 | reducers: { 4 | increment: (state, payload) => state + payload, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/migration-redux-3/src/models/sharks.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: 0, 3 | reducers: { 4 | increment: (state, payload) => state + payload, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/migration-redux-3/src/redux.js: -------------------------------------------------------------------------------- 1 | import { connect as reduxConnect, createSelectorHook, createDispatchHook } from 'react-redux'; 2 | import store from './store'; 3 | 4 | export const useSelector = createSelectorHook(store.context); 5 | export const useDispatch = createDispatchHook(store.context); 6 | export const connect = reduxConnect; 7 | -------------------------------------------------------------------------------- /examples/migration-redux-3/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from '@ice/store'; 2 | 3 | import sharks from './models/sharks'; 4 | import dolphins from './models/dolphins'; 5 | 6 | const store = createStore( 7 | { sharks, dolphins }, 8 | ); 9 | 10 | export default store; 11 | -------------------------------------------------------------------------------- /examples/migration-redux-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icestore-migration-part-4", 3 | "version": "1.0.0", 4 | "description": "Using icestore with react-redux & icestore.", 5 | "private": true, 6 | "dependencies": { 7 | "@ice/store": "^2.0.0", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-redux": "^7.2.0" 11 | }, 12 | "devDependencies": { 13 | "react-scripts": "^3.4.0" 14 | }, 15 | "scripts": { 16 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 17 | "build": "react-scripts build" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/migration-redux-4/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | redux Migration: Part 1 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/migration-redux-4/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect, useSelector /* useDispatch */ } from './redux'; 3 | import store from './store'; 4 | 5 | const Count = props => { 6 | const sharks = useSelector(state => state.sharks); 7 | // const dispatch = useDispatch(); 8 | // const sharks = store.useModelState('sharks'); 9 | const dispatchers = store.useModelDispatchers('sharks'); 10 | 11 | return ( 12 |
13 |
14 |
15 |

Sharks

16 |

{sharks}

17 | 18 |
19 |
20 |

Dolphins

21 |

{props.dolphins}

22 | 23 |
24 |
25 |

Using react-redux & icestore

26 |
27 | ); 28 | }; 29 | 30 | const mapState = state => ({ 31 | dolphins: state.dolphins, 32 | }); 33 | 34 | // const mapDispatch = dispatch => ({ 35 | // incrementDolphins: () => dispatch.dolphins.increment(1) 36 | // }); 37 | 38 | const WrapperedCount = connect( 39 | mapState, 40 | undefined, 41 | undefined, 42 | { context: store.context }, 43 | )(Count); 44 | 45 | export default store.withModelDispatchers('dolphins')(WrapperedCount); 46 | -------------------------------------------------------------------------------- /examples/migration-redux-4/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import store from './store'; 4 | 5 | import App from './App'; 6 | 7 | const Root = () => ( 8 | 9 | 10 | 11 | ); 12 | 13 | ReactDOM.render(, document.querySelector('#root')); 14 | -------------------------------------------------------------------------------- /examples/migration-redux-4/src/models/dolphins.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: 0, 3 | reducers: { 4 | increment: (state) => state + 1, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/migration-redux-4/src/models/sharks.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: 0, 3 | reducers: { 4 | increment: (state, payload) => state + payload, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/migration-redux-4/src/redux.js: -------------------------------------------------------------------------------- 1 | import { connect as reduxConnect, createSelectorHook, createDispatchHook } from 'react-redux'; 2 | import store from './store'; 3 | 4 | export const useSelector = createSelectorHook(store.context); 5 | export const useDispatch = createDispatchHook(store.context); 6 | export const connect = reduxConnect; 7 | -------------------------------------------------------------------------------- /examples/migration-redux-4/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore } from '@ice/store'; 2 | 3 | import sharks from './models/sharks'; 4 | import dolphins from './models/dolphins'; 5 | 6 | const store = createStore( 7 | { sharks, dolphins }, 8 | ); 9 | 10 | export default store; 11 | -------------------------------------------------------------------------------- /examples/todos/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /examples/todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todos", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ice/store": "^2.0.0", 7 | "lodash": "^4.17.15", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6" 10 | }, 11 | "devDependencies": { 12 | "@types/jest": "^24.0.0", 13 | "@types/node": "^12.0.0", 14 | "@types/react": "^16.9.0", 15 | "@types/react-dom": "^16.9.0", 16 | "react-scripts": "^3.4.0", 17 | "typescript": "^3.7.5", 18 | "utility-types": "^3.10.0" 19 | }, 20 | "scripts": { 21 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 22 | "build": "react-scripts build" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/todos/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Todos App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/todos/src/components/AddTodo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import store from '../store'; 3 | 4 | const { useModelDispatchers } = store; 5 | 6 | export default function () { 7 | const { add } = useModelDispatchers('todos'); 8 | let input; 9 | 10 | return ( 11 |
12 |
{ 13 | e.preventDefault(); 14 | if (!input.value.trim()) { 15 | return; 16 | } 17 | add({ text: input.value, completed: true }); 18 | input.value = ''; 19 | }}> 20 | input = node} /> 21 | 24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/todos/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { VisibilityFilters } from '../models/visibilityFilter'; 4 | import store from '../store'; 5 | 6 | const Link = ({ active, children, onClick }) => ( 7 | 17 | ); 18 | 19 | export default function Footer() { 20 | const [state, dispatchers] = store.useModel('visibilityFilter'); 21 | return ( 22 |
23 | Show: 24 | { 25 | Object.keys(VisibilityFilters).map((key) => { 26 | return ( dispatchers.setState(key)}> 27 | {key.toLowerCase()} 28 | ); 29 | }) 30 | } 31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/todos/src/components/Todo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function ({ completed, text, onAsyncRemove, onRemove, onToggle, isLoading }) { 4 | return ( 5 |
  • 6 | 15 | 21 | 28 |
  • 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /examples/todos/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import store from '../store'; 3 | import Todo from './Todo'; 4 | import { VisibilityFilters } from '../models/visibilityFilter'; 5 | 6 | const { useModel, useModelEffectsLoading } = store; 7 | 8 | const getVisibleTodos = (todos, filter) => { 9 | switch (filter) { 10 | case VisibilityFilters.ALL: 11 | return todos; 12 | case VisibilityFilters.COMPLETED: 13 | return todos.filter(t => t.completed); 14 | case VisibilityFilters.ACTIVE: 15 | return todos.filter(t => !t.completed); 16 | default: 17 | throw new Error(`Unknown filter: ${filter}`); 18 | } 19 | }; 20 | 21 | export default function TodoList() { 22 | const [todos, dispatchers] = useModel('todos'); 23 | const [visibilityFilter] = useModel('visibilityFilter'); 24 | const effectsLoading = useModelEffectsLoading('todos'); 25 | 26 | const { refresh, asyncRemove, remove, toggle } = dispatchers; 27 | const visableTodos = getVisibleTodos(todos, visibilityFilter); 28 | 29 | useEffect(() => { 30 | refresh(); 31 | // eslint-disable-next-line 32 | }, []); 33 | 34 | const noTaskView =
    No task
    ; 35 | const loadingView =
    Loading...
    ; 36 | const taskView = visableTodos.length ? ( 37 |
      38 | {visableTodos.map(({ text, completed }, index) => ( 39 | asyncRemove(index)} 44 | onRemove={() => remove(index)} 45 | onToggle={() => toggle(index)} 46 | isLoading={effectsLoading.asyncRemove} 47 | /> 48 | ))} 49 |
    50 | ) : noTaskView; 51 | 52 | return effectsLoading.refresh ? loadingView : taskView; 53 | } 54 | -------------------------------------------------------------------------------- /examples/todos/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import store from './store'; 4 | import TodoList from './components/TodoList'; 5 | import AddTodo from './components/AddTodo'; 6 | import Footer from './components/Footer'; 7 | 8 | const { Provider } = store; 9 | 10 | function App() { 11 | return ( 12 | 13 | 14 | 15 |