diff --git a/README.md b/README.md index c70f7c0f..47f3cb22 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ English | [简体中文](./README.zh-CN.md) [![NPM downloads](http://img.shields.io/npm/dm/@ice/store.svg?style=flat)](https://npmjs.org/package/@ice/store) [![Known Vulnerabilities](https://snyk.io/test/npm/@ice/store/badge.svg)](https://snyk.io/test/npm/@ice/store) [![David deps](https://img.shields.io/david/ice-lab/icestore.svg?style=flat-square)](https://david-dm.org/ice-lab/icestore) +[![codecov](https://codecov.io/gh/ice-lab/icestore/branch/master/graph/badge.svg)](https://codecov.io/gh/ice-lab/icestore) @@ -53,7 +54,7 @@ const counter = { decrement:(prevState) => prevState - 1, }, effects: () => ({ - async decrementAsync() { + async asyncDecrement() { await delay(1000); this.decrement(); }, @@ -71,12 +72,12 @@ const store = createStore(models); const { useModel } = store; function Counter() { const [ count, dispatchers ] = useModel('counter'); - const { increment, decrementAsync } = dispatchers; + const { increment, asyncDecrement } = dispatchers; return (
{count} - +
); } @@ -103,17 +104,19 @@ icestore requires React 16.8.0 or later. npm install @ice/store --save ``` -## API +## Documents -[docs/api](./docs/api.md) +- [API](./docs/api.md) +- [Recipes](./docs/recipes.md) +- [Upgrade Guidelines](./docs/upgrade-guidelines.md) +- [Migration](./docs/migration.md) -## Recipes +## Examples -[docs/recipes](./docs/recipes.md) - -## Upgrade Guidelines - -[docs/upgrade-guidelines](./docs/upgrade-guidelines.md) +- [Counter](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/counter) +- [Todos](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/todos) +- [Class Component Support](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/classComponent) +- [withModel](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/withModel) ## Browser Compatibility diff --git a/README.zh-CN.md b/README.zh-CN.md index ffbc66da..aa313d64 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -10,6 +10,7 @@ [![NPM downloads](http://img.shields.io/npm/dm/@ice/store.svg?style=flat)](https://npmjs.org/package/@ice/store) [![Known Vulnerabilities](https://snyk.io/test/npm/@ice/store/badge.svg)](https://snyk.io/test/npm/@ice/store) [![David deps](https://img.shields.io/david/ice-lab/icestore.svg?style=flat-square)](https://david-dm.org/ice-lab/icestore) +[![codecov](https://codecov.io/gh/ice-lab/icestore/branch/master/graph/badge.svg)](https://codecov.io/gh/ice-lab/icestore)
@@ -53,7 +54,7 @@ const counter = { decrement:(prevState) => prevState - 1, }, effects: () => ({ - async decrementAsync() { + async asyncDecrement() { await delay(1000); this.decrement(); }, @@ -71,12 +72,12 @@ const store = createStore(models); const { useModel } = store; function Counter() { const [ count, dispatchers ] = useModel('counter'); - const { increment, decrementAsync } = dispatchers; + const { increment, asyncDecrement } = dispatchers; return (
{count} - +
); } @@ -103,17 +104,19 @@ ReactDOM.render(, rootElement); npm install @ice/store --save ``` -## API +## 文档 -[docs/api](./docs/api.zh-CN.md) +- [API](./docs/api.zh-CN.md) +- [更多技巧](./docs/recipes.zh-CN.md) +- [从老版本升级](./docs/upgrade-guidelines.zh-CN.md) +- [从其他方案迁移](./docs/migration.zh-CN.md) -## 更多技巧 +## 示例 -[docs/recipes](./docs/recipes.zh-CN.md) - -## 从老版本升级 - -[docs/upgrade-guidelines](./docs/upgrade-guidelines.zh-CN.md) +- [Counter](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/counter) +- [Todos](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/todos) +- [Class Component Support](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/classComponent) +- [withModel](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/withModel) ## 浏览器支持 diff --git a/codecov.yml b/codecov.yml index 1be6c967..9255ed44 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,12 @@ -comment: off +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: false + require_base: no + require_head: yes + branches: + - "master" + coverage: status: project: diff --git a/docs/api.md b/docs/api.md index c63d5079..bb8bef73 100644 --- a/docs/api.md +++ b/docs/api.md @@ -51,25 +51,23 @@ const model = { ##### reducers -`reducers: { [string]: (prevState, payload) => any }` +`reducers: { [string]: (state, payload) => any }` An object of functions that change the model's state. These functions take the model's previous state and a payload, use mutable method to achieve immutable state. These should be pure functions relying only on the state and payload args to compute the next state. For code that relies on the "outside world" (impure functions like api calls, etc.), use effects. +e.g.: + ```js -const todo = { +const todos = { state: [ { - todo: 'Learn typescript', + title: 'Learn typescript', done: true, }, - { - todo: 'Try immer', - done: false, - }, ], reducers: { - done(state) { - state.push({ todo: 'Tweet about it' }); // array updated directly + foo(state) { + state.push({ title: 'Tweet about it' }); // array updated directly state[1].done = true; }, }, @@ -92,11 +90,43 @@ const count = { See [docs/recipes](./recipes.md#immutable-description) for more details. +The second parameter of reducer is the parameter passed when calling: + +```js +const todos = { + state: [ + { + title: 'Learn typescript', + done: true, + }, + ], + reducers: { + // correct + add(state, todo) { + state.push(todo); + }, + // wrong + add(state, title, done) { + state.push({ title, done }); + }, + }, +}; + +// use: +function Component() { + const { add } = store.useModelDispathers('todos'); + function handleClick () { + add({ title: 'Learn React', done: false }); // correct + add('Learn React', false); // wrong + } +} +``` + ##### effects `effects: (dispatch) => ({ [string]: (payload, rootState) => void })` -An object of functions that can handle the world outside of the model. Effects provide a simple way of handling async actions when used with async/await. In effects, call `this.reducerFoo` to update model's state: +An object of functions that can handle the world outside of the model. These functions take payload and rootstate, sprovide a simple way of handling async actions when used with async/await. In effects, call `this.reducerFoo` to update model's state: ```js const counter = { @@ -105,7 +135,7 @@ const counter = { decrement:(prevState) => prevState - 1, }, effects: () => ({ - async decrementAsync() { + async asyncDecrement() { await delay(1000); // do some asynchronous operations this.decrement(); // pass the result to a local reducer }, @@ -397,6 +427,19 @@ export default withModel( )(TodoList); ``` +#### useModelState + +`useModelState(name: string): state` + +The hooks use the state of the model and subscribe to its updates. + +```js +function FunctionComponent() { + const state = useModelState('counter'); + console.log(state.value); +} +``` + #### useModelDispatchers `useModelDispatchers(name: string): dispatchers` @@ -573,3 +616,71 @@ function FunctionComponent() { ); } ``` + +## withModel + +`withModel(model, mapModelToProps?, options?)(ReactFunctionComponent)` + +This method is used to quickly use model in component. + +```js +import { withModel } from '@ice/store'; +import model from './model'; + +function Todos({ model }) { + const { + useState, + useDispatchers, + useEffectsState, + getState, + getDispatchers, + } = model; + const [ state, dispatchers ] = useValue(); +} + +export default withModel(model)(Todos); +``` + +### Arguments + +#### modelConfig + +Consistent with modelConfig in the createStore. + +#### mapModelToProps + +`mapModelToProps = (model) => ({ model })` + +Use this function to customize the value mapped to the component, for example: + +```js +import { withModel } from '@ice/store'; +import model from './model'; + +function Todos({ todo }) { + const [ state, dispatchers ] = todo.useValue(); +} + +export default withModel(model, function(model) { + return { todo: model }; +})(Todos); +``` + +#### options + +The same with createStore. + +### Returns + +- useValue +- useState +- useDispathers +- useEffectsState +- getValue +- getState +- getDispatchers +- withValue +- withDispatchers +- withModelEffectsState + +Its usage refers to the return value of createStore. diff --git a/docs/api.zh-CN.md b/docs/api.zh-CN.md index 1fef6724..0144c1d0 100644 --- a/docs/api.zh-CN.md +++ b/docs/api.zh-CN.md @@ -37,6 +37,20 @@ const { #### models +`createStore({ [string]: modelConfig });` + +```js +import { createStore } from '@ice/store' + +const count = { + state: 0, +}; + +createStore({ + count +}); +``` + ##### state `state: any`: 必填 @@ -51,32 +65,32 @@ const model = { ##### reducers -`reducers: { [string]: (prevState, payload) => any }` +`reducers: { [string]: (state, payload) => any }` + +一个改变该模型状态的函数集合。这些方法以模型的上一次 state 和一个 payload 作为入参,在方法中使用可变的方式来更新状态。 +这些方法应该是仅依赖于 state 和 payload 参数来计算下一个 state 的纯函数。对于有副作用的函数,请使用 effects。 -一个改变该模型状态的函数集合。这些方法以模型的上一次 state 和一个 payload 作为入参,在方法中使用可变的方式来更新状态。这些方法应该是仅依赖于 state 和 payload 参数来计算下一个 state 的纯函数。对于有副作用的函数,请使用 effects。 +一个简单的示例: ```js -const todo = { +const todos = { state: [ { - todo: 'Learn typescript', + title: 'Learn typescript', done: true, }, - { - todo: 'Try immer', - done: false, - }, ], reducers: { - done(state) { - state.push({ todo: 'Tweet about it' }); // 直接更新了数组 + foo(state) { + state.push({ title: 'Tweet about it' }); // 直接更新了数组 state[1].done = true; }, }, -} +}; ``` -icestore 内部是通过调用 [immer](https://github.com/immerjs/immer) 来实现可变状态的。Immer 只支持对普通对象和数组的变化检测,所以像字符串或数字这样的类型需要返回一个新值。 例如: +icestore 内部是通过调用 [immer](https://github.com/immerjs/immer) 来实现可变状态的。 +Immer 只支持对普通对象和数组的变化检测,所以像字符串或数字这样的类型需要返回一个新值。 例如: ```js const count = { @@ -92,11 +106,43 @@ const count = { 参考 [docs/recipes](./recipes.zh-CN.md#可变状态的说明) 了解更多。 +reducer 的第二个参数即是调用时传递的参数: + +```js +const todos = { + state: [ + { + title: 'Learn typescript', + done: true, + }, + ], + reducers: { + // 正确用法 + add(state, todo) { + state.push(todo); + }, + // 错误用法 + add(state, title, done) { + state.push({ title, done }); + }, + }, +}; + +// 使用时: +function Component() { + const { add } = store.useModelDispathers('todos'); + function handleClick () { + add({ title: 'Learn React', done: false }); // 正确用法 + add('Learn React', false); // 错误用法 + } +} +``` + ##### effects `effects: (dispatch) => ({ [string]: (payload, rootState) => void })` -一个可以处理该模型副作用的函数集合。Effects 适用于进行异步调用、[模型联动](recipes.zh-CN.md#模型联动)等场景。在 effects 内部,通过调用 `this.reducerFoo` 来更新模型状态: +一个可以处理该模型副作用的函数集合。这些方法以 payload 和 rootState 作为入参,适用于进行异步调用、[模型联动](recipes.zh-CN.md#模型联动)等场景。在 effects 内部,通过调用 `this.reducerFoo` 来更新模型状态: ```js const counter = { @@ -105,7 +151,7 @@ const counter = { decrement:(prevState) => prevState - 1, }, effects: () => ({ - async decrementAsync() { + async asyncDecrement() { await delay(1000); // 进行一些异步操作 this.decrement(); // 调用模型 reducers 内的方法来更新状态 }, @@ -113,7 +159,7 @@ const counter = { }; ``` -> 注意:如果您正在使用 TypeScript 并且配置了编译选项 `noImplicitThis: ture`,则会遇到类似 "Property 'setState' does not exist on type" 的编译错误。您可以通过删除该编译选项,或者使用下面示例中的 `dispatch.model.reducer` 来避免此错误。 +> 注意:如果您正在使用 TypeScript ,并且配置了编译选项 `noImplicitThis: ture`,则会遇到类似 "Property 'setState' does not exist on type" 的编译错误。您可以通过删除该编译选项,或者使用下面示例中的 `dispatch.model.reducer` 来避免此错误。 ###### 同名处理 @@ -397,6 +443,19 @@ export default withModel( )(TodoList); ``` +#### useModelState + +`useModelState(name: string): state` + +通过该 hooks 使用模型的状态并订阅其更新。 + +```js +function FunctionComponent() { + const state = useModelState('counter'); + console.log(state.value); +} +``` + #### useModelDispatchers `useModelDispatchers(name: string): dispatchers` @@ -573,3 +632,71 @@ function FunctionComponent() { ); } ``` + +## withModel + +`withModel(model, mapModelToProps?, options?)(ReactFunctionComponent)` + +该方法用于在组件中快速使用 Model。 + +```js +import { withModel } from '@ice/store'; +import model from './model'; + +function Todos({ model }) { + const { + useState, + useDispatchers, + useEffectsState, + getState, + getDispatchers, + } = model; + const [ state, dispatchers ] = useValue(); +} + +export default withModel(model)(Todos); +``` + +### 参数 + +#### modelConfig + +与 createStore 方法中的 modelConfig 一致。 + +#### mapModelToProps + +`mapModelToProps = (model) => ({ model })` + +使用该函数来自定义映射到组件中的值,使用示例: + +```js +import { withModel } from '@ice/store'; +import model from './model'; + +function Todos({ todo }) { + const [ state, dispatchers ] = todo.useValue(); +} + +export default withModel(model, function(model) { + return { todo: model }; +})(Todos); +``` + +#### options + +与 createStore 方法中的 options 一致。 + +### 返回值 + +- useValue +- useState +- useDispathers +- useEffectsState +- getValue +- getState +- getDispatchers +- withValue +- withDispatchers +- withModelEffectsState + +其用法参考 createStore 的返回值。 diff --git a/docs/migration.md b/docs/migration.md new file mode 100644 index 00000000..61f0a477 --- /dev/null +++ b/docs/migration.md @@ -0,0 +1,280 @@ +--- +id: migration +title: Migration +--- + +## Migrating From Redux + +We provide a gradual solution to allow your project to be partially migrated from Redux to icestore. + +> Requires React 16.8.0 or later & React-Redux 7.0.0 or later. + +### Step 1: Migrating createStore + +See: [CodeSandbox](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/migration-redux-1?module=/src/index.js) + +#### Redux createStore + +```js +import { createStore, combineReducers } from 'redux'; + +import sharks from './reducers/sharks'; +import dolphins from './reducers/dolphins'; + +const rootReducer = combineReducers({ + sharks, + dolphins +}); +const store = createStore(rootReducer); +``` + +#### icestore createStore + +```js +import { createStore } from 'icestore'; + +import sharks from './reducers/sharks'; +import dolphins from './reducers/dolphins'; + +// Using createStore from icestore +const store = createStore( + { /* No models */ }, + { + redux: { + reducers: { + sharks, + dolphins + } + } + } +); +``` + +### Step 2: Mix reducers & models + +You can locally and incrementally replace the Redux Reducer in your project with icestore Model. + +See: [CodeSandbox](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/migration-redux-2?module=/src/index.js) + +#### Declaration + +##### Redux's Reducer + +```js +const INCREMENT = 'sharks/increment'; + +export const incrementSharks = (payload) => ({ + type: INCREMENT, + payload, +}); + +export default (state = 0, action) => { + switch(action.type) { + case INCREMENT: + return state + action.payload; + default: + return state; + } +}; +``` + +##### icestore's Model + +```js +export default { + state: 0, + reducers: { + increment: (state, payload) => state + payload + } +} +``` + +#### Consumer + +##### Redux in mapDispatch + +```js +import { connect } from 'react-redux'; +import { incrementSharks } from './reducers/sharks'; +import { incrementDolphins } from './reducers/dolphins'; + +const mapDispatch = dispatch => ({ + incrementSharks: () => dispatch(incrementSharks(1)), + incrementDolphins: () => dispatch(incrementDolphins(1)) +}); + +export default connect(undefined, mapDispatch)(ReactComponent); +``` + +##### icestore in mapDispatch + +```js +import { connect } from 'react-redux'; +import { incrementDolphins } from './reducers/dolphins'; + +const mapDispatch = dispatch => ({ + // important!!! + incrementSharks: () => dispatch.sharks.increment(1), + incrementDolphins: () => dispatch(incrementDolphins(1)), +}); + +export default connect(undefined, mapDispatch)(ReactComponent); +``` + +### Step 3: Migrating Provider + +See: [CodeSandbox](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/migration-redux-3?module=/src/index.js) + +#### Migrating from react-redux Provider + +##### react-redux + +```js +import { Provider } from 'react-redux'; +import App from './App'; +import store from './store'; + +const Root = () => ( + + + +); +``` + +##### icestore + +```js +import App from './App'; +import store from './store'; + +const Root = () => ( + + + +); +``` + +#### react-redux Hooks compatible + +##### Origin + +```js +import { useSelector, useDispatch } from 'react-redux'; + +export default function() { + const sharks = useSelector(state => state.sharks); + const dispatch = useDispatch(); + dispatch.sharks.increment(); +} +``` + +##### Now + +```js +import { createSelectorHook, createDispatchHook } from 'react-redux'; +import store from './store'; + +// Create Redux hooks using the context provided by the store +const useSelector = createSelectorHook(store.context); +const useDispatch = createDispatchHook(store.context); + +export default function() { + const sharks = useSelector(state => state.sharks); + const dispatch = useDispatch(); + dispatch.sharks.increment(); +} +``` + +#### react-redux connect compatible + +##### Origin + +```js +import { connect } from 'react-redux'; + +export default connect( + mapState, + mapDispatch +)(ReactComponent); +``` + +##### Now + +```js +import { connect } from 'react-redux'; +import store from './store'; + +export default connect( + mapState, + mapDispatch, + mergeProps, + + // Pass the context provided by the store to the connect function + { context: store.context } +)(ReactComponent); +``` + +### Step 4: Migrating From react-redux + +You can locally and incrementally replace the react Redux API in your project with the icestore API. + +See: [CodeSandbox](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/migration-redux-4?module=/src/index.js) + +#### Migrating From react-redux Hooks + +```js +import { useSelector, useDispatch } from './reudx'; +import store from './store'; + +function Component (){ + // const sharks = useSelector(state => state.sharks); + const sharks = store.useModelState('sharks'); + // const dispatch = useDispatch(); + // dispatch.sharks.increment(); + const dispatchers = store.useModelDispathers('sharks'); + dispatchers.increment(); +} +``` + +#### Migrating From react-redux connect + +##### Origin + +```js +import { connect } from 'react-reudx'; + +function Count(props) { + console.log(props.dolphins); + props.incrementDolphins(); +} + +const mapState = state => ({ + dolphins: state.dolphins +}); + +const mapDispatch = dispatch => ({ + incrementDolphins: dispatch.dolphins.increment +}); + +export default connect( + mapState, + mapDispatch, + undefined, + { context: store.context } +)(Count); +``` + +##### Now + +```js +import store from './store'; +const { withModel } = store; + +function Count(props) { + const [dolphins, { increment }] = props.dolphins; + console.log(dolphins); + increment(); +} + +withModel('dolphins')(Count); +``` diff --git a/docs/migration.zh-CN.md b/docs/migration.zh-CN.md new file mode 100644 index 00000000..51ae5d05 --- /dev/null +++ b/docs/migration.zh-CN.md @@ -0,0 +1,280 @@ +--- +id: migration +title: Migration +--- + +## 从 Redux 迁移 + +我们提供了渐进式的方案使得您的项目可以局部从 Redux 迁移到 icestore。 + +> 请确保在项目中使用的 react-redux >= 7.0.0 且 react >= 16.8.0 。 + +### 第一步:替换 createStore 方法 + +参考:[CodeSandbox](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/migration-redux-1?module=/src/index.js) + +#### Redux 创建 Store 的方式 + +```js +import { createStore, combineReducers } from 'redux'; + +import sharks from './reducers/sharks'; +import dolphins from './reducers/dolphins'; + +const rootReducer = combineReducers({ + sharks, + dolphins +}); +const store = createStore(rootReducer); +``` + +#### icestore 创建 Store 的方式 + +```js +import { createStore } from 'icestore'; + +import sharks from './reducers/sharks'; +import dolphins from './reducers/dolphins'; + +// 使用 icestore 的 createStore 方法 +const store = createStore( + { /* No models */ }, + { + redux: { + reducers: { + sharks, + dolphins + } + } + } +); +``` + +### 第二步:将 reducer 替换为 model + +您可以局部渐进式地将项目中的 Redux reducer 替换为 icestore model。 + +参考:[CodeSandbox](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/migration-redux-2?module=/src/index.js) + +#### 声明层 + +##### Redux 中 reducer 的声明 + +```js +const INCREMENT = 'sharks/increment'; + +export const incrementSharks = (payload) => ({ + type: INCREMENT, + payload, +}); + +export default (state = 0, action) => { + switch(action.type) { + case INCREMENT: + return state + action.payload; + default: + return state; + } +}; +``` + +##### icestore 中 model 的声明 + +```js +export default { + state: 0, + reducers: { + increment: (state, payload) => state + payload + } +} +``` + +#### 消费层 + +##### Redux 中 mapDispatch 的返回值 + +```js +import { connect } from 'react-redux'; +import { incrementSharks } from './reducers/sharks'; +import { incrementDolphins } from './reducers/dolphins'; + +const mapDispatch = dispatch => ({ + incrementSharks: () => dispatch(incrementSharks(1)), + incrementDolphins: () => dispatch(incrementDolphins(1)) +}); + +export default connect(undefined, mapDispatch)(ReactComponent); +``` + +##### icestore 中 mapDispatch 的返回值 + +```js +import { connect } from 'react-redux'; +import { incrementDolphins } from './reducers/dolphins'; + +const mapDispatch = dispatch => ({ + // 注意这一行的区别! + incrementSharks: () => dispatch.sharks.increment(1), + incrementDolphins: () => dispatch(incrementDolphins(1)), +}); + +export default connect(undefined, mapDispatch)(ReactComponent); +``` + +### 第三步:替换 Provider + +参考:[CodeSandbox](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/migration-redux-3?module=/src/index.js) + +#### 将 react-redux Provider 替换为 icestore Provider + +##### react-redux + +```js +import { Provider } from 'react-redux'; +import App from './App'; +import store from './store'; + +const Root = () => ( + + + +); +``` + +##### icestore + +```js +import App from './App'; +import store from './store'; + +const Root = () => ( + + + +); +``` + +#### 兼容 react-redux Hooks + +##### 原 react-redux Hooks 用法 + +```js +import { useSelector, useDispatch } from 'react-redux'; + +export default function() { + const sharks = useSelector(state => state.sharks); + const dispatch = useDispatch(); + dispatch.sharks.increment(); +} +``` + +##### 兼容做法 + +```js +import { createSelectorHook, createDispatchHook } from 'react-redux'; +import store from './store'; + +// 使用 store 提供的 context 创建 Redux Hooks +const useSelector = createSelectorHook(store.context); +const useDispatch = createDispatchHook(store.context); + +export default function() { + const sharks = useSelector(state => state.sharks); + const dispatch = useDispatch(); + dispatch.sharks.increment(); +} +``` + +#### 兼容 react-redux connect + +##### 原 react-redux connect 用法 + +```js +import { connect } from 'react-redux'; + +export default connect( + mapState, + mapDispatch +)(ReactComponent); +``` + +##### 兼容做法 + +```js +import { connect } from 'react-redux'; +import store from './store'; + +export default connect( + mapState, + mapDispatch, + mergeProps, + + // 传递 store 提供的 context 给 connect 函数 + { context: store.context } +)(ReactComponent); +``` + +### 第四步:将 react-redux 替换为 icestore + +您可以局部渐进式地将项目中的 react-redux API 替换为 icestore API。 + +参考:[CodeSandbox](https://codesandbox.io/s/github/ice-lab/icestore/tree/master/examples/migration-redux-4?module=/src/index.js) + +#### 替换 react-redux Hooks + +```js +import { useSelector, useDispatch } from './reudx'; +import store from './store'; + +function Component (){ + // const sharks = useSelector(state => state.sharks); + const sharks = store.useModelState('sharks'); + // const dispatch = useDispatch(); + // dispatch.sharks.increment(); + const dispatchers = store.useModelDispathers('sharks'); + dispatchers.increment(); +} +``` + +#### 替换 react-redux connect + +##### 原 react-redux connect 用法 + +```js +import { connect } from 'react-reudx'; + +function Count(props) { + console.log(props.dolphins); + props.incrementDolphins(); +} + +const mapState = state => ({ + dolphins: state.dolphins +}); + +const mapDispatch = dispatch => ({ + incrementDolphins: dispatch.dolphins.increment +}); + +export default connect( + mapState, + mapDispatch, + undefined, + { context: store.context } +)(Count); +``` + +##### 使用 icestore API + +```js +import store from './store'; +const { withModel } = store; + +function Count(props) { + const [dolphins, { increment }] = props.dolphins; + console.log(dolphins); + increment(); +} + +withModel('dolphins')(Count); +``` diff --git a/docs/recipes.md b/docs/recipes.md index c49bce68..36506a0d 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -134,7 +134,7 @@ import store from '@/store'; const user = { effects: dispatch => ({ - async addByAsync(payload, state) { + async asyncAdd(payload, state) { dispatch.todos.addTodo(payload); // Call methods of other models to update their state const todos = store.getModelState('todos'); // Get the latest state of the updated model } diff --git a/docs/recipes.zh-CN.md b/docs/recipes.zh-CN.md index 670bd6df..83041df2 100644 --- a/docs/recipes.zh-CN.md +++ b/docs/recipes.zh-CN.md @@ -132,7 +132,7 @@ import store from '@/store'; const user = { effects: dispatch => ({ - async addByAsync(payload, state) { + async asyncAdd(payload, state) { dispatch.todos.addTodo(payload); // 调用其他模型的方法更新其状态 const todos = store.getModelState('todos'); // 获取更新后的模型最新状态 } diff --git a/docs/upgrade-guidelines.md b/docs/upgrade-guidelines.md index 6cc5778a..a60bf03c 100644 --- a/docs/upgrade-guidelines.md +++ b/docs/upgrade-guidelines.md @@ -5,7 +5,7 @@ title: Upgrade Guidelines English | [简体中文](./upgrade-guidelines.zh-CN.md) -## 1.2.0 to 1.3.0 +## Upgrade from 1.2.0 to 1.3.0 From 1.2.0 to 1.3.0 is fully compatible, but we recommend that you use the new API in incremental code. We will remove the deprecated API in future versions. @@ -123,7 +123,7 @@ export default withModelDispatchers('todos')(TodoList); - ModelEffectsState => ExtractIModelEffectsStateFromModelConfig - UseModelValue => ExtractIModelFromModelConfig -## 1.0.0 to 1.3.0 +## Upgrade from 1.0.0 to 1.3.0 From 1.0.0 to 1.3.0 is fully compatible, but we recommend that you use the new API in incremental code. We will remove the deprecated API in future versions. @@ -139,13 +139,13 @@ const counter = { }, actions: { increment:(state) => ({ value: state.value + 1 }), - async incrementAsync(state, payload, actions, globalActions) { + async asyncIncrement(state, payload, actions, globalActions) { console.log(state); // 0 await delay(1000); actions.increment(); globalActions.todo.refresh(); }, - async decrementAsync(state) { + async asyncDecrement(state) { await delay(1000); return { value: state.value - 1 }; }, @@ -164,13 +164,13 @@ const counter = { increment:(prevState) => ({ value: prevState.value + 1 }), }, effects: (dispatch) => ({ - async incrementAsync(payload, rootState) { + async asyncIncrement(payload, rootState) { console.log(rootState.counter); // 0 await delay(1000); this.increment(); dispatch.todo.refresh(); }, - async decrementAsync(payload, rootState) { + async asyncDecrement(payload, rootState) { await delay(1000); this.setState({ value: rootState.counter.value - 1 }); // setState is a built-in reducer }, @@ -238,7 +238,7 @@ class TodoList extends Component { export default withModelEffectsState('todos')(TodoList); ``` -## 0.x.x to 1.x.x +## Upgrade from 0.x.x to 1.x.x ### Define Model diff --git a/docs/upgrade-guidelines.zh-CN.md b/docs/upgrade-guidelines.zh-CN.md index 7f8d6241..089ccc8c 100644 --- a/docs/upgrade-guidelines.zh-CN.md +++ b/docs/upgrade-guidelines.zh-CN.md @@ -5,7 +5,7 @@ title: Upgrade Guidelines [English](./upgrade-guidelines.md) | 简体中文 -## 1.2.0 to 1.3.0 +## 从 1.2.0 升级到 1.3.0 1.3.0 是完全向下兼容的,但是我们推荐您在新增代码中使用最新的 API。 @@ -126,7 +126,7 @@ export default withModelDispatchers('todos')(TodoList); - ModelEffectsState => ExtractIModelEffectsStateFromModelConfig - UseModelValue => ExtractIModelFromModelConfig -## 1.0.0 to 1.3.0 +## 从 1.0.0 升级到 1.3.0 1.3.0 是完全向下兼容的,但是我们推荐您在新增代码中使用最新的 API。 @@ -143,13 +143,13 @@ const counter = { }, actions: { increment:(state) => ({ value: state.value + 1 }), - async incrementAsync(state, payload, actions, globalActions) { + async asyncIncrement(state, payload, actions, globalActions) { console.log(state); // 0 await delay(1000); actions.increment(); globalActions.todo.refresh(); }, - async decrementAsync(state) { + async asyncDecrement(state) { await delay(1000); return { value: state.value - 1 }; }, @@ -168,13 +168,13 @@ const counter = { increment:(prevState) => ({ value: prevState.value + 1 }), }, effects: (dispatch) => ({ - async incrementAsync(payload, rootState) { + async asyncIncrement(payload, rootState) { console.log(rootState.counter); // 0 await delay(1000); this.increment(); dispatch.todo.refresh(); }, - async decrementAsync(payload, rootState) { + async asyncDecrement(payload, rootState) { await delay(1000); this.setState({ value: rootState.counter.value - 1 }); // setState 是一个内置的 reducer }, @@ -242,7 +242,7 @@ class TodoList extends Component { export default withModelEffectsState('todos')(TodoList); ``` -## 0.x.x to 1.x.x +## 从 0.x.x 升级到 1.x.x 从 0.x.x 到 1.x.x 是不兼容的。您可以选择性地进行升级。 diff --git a/examples/classComponent/.gitgnore b/examples/classComponent/.gitgnore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/examples/classComponent/.gitgnore @@ -0,0 +1 @@ +build diff --git a/examples/classComponent/package.json b/examples/classComponent/package.json new file mode 100644 index 00000000..256cd72f --- /dev/null +++ b/examples/classComponent/package.json @@ -0,0 +1,35 @@ +{ + "name": "counter", + "version": "1.0.0", + "private": true, + "dependencies": { + "@ice/store": "^1.4.0", + "react": "^16.8.6", + "react-dom": "^16.8.6" + }, + "devDependencies": { + "@types/jest": "^24.0.0", + "@types/node": "^12.0.0", + "@types/react": "^16.9.0", + "@types/react-dom": "^16.9.0", + "react-scripts": "3.4.0", + "typescript": "^3.7.5", + "utility-types": "^3.10.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/classComponent/public/index.html b/examples/classComponent/public/index.html new file mode 100644 index 00000000..492089d1 --- /dev/null +++ b/examples/classComponent/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + Counter App + + + +
+ + + diff --git a/examples/classComponent/src/components/Product/Class.tsx b/examples/classComponent/src/components/Product/Class.tsx new file mode 100644 index 00000000..f6e3ccdc --- /dev/null +++ b/examples/classComponent/src/components/Product/Class.tsx @@ -0,0 +1,42 @@ +/* eslint-disable react/prefer-stateless-function */ +import React from 'react'; +import { Assign } from 'utility-types'; +import { withModel, ExtractIModelAPIsFromModelConfig, ExtractIModelFromModelConfig } from '@ice/store'; +import Product from './Product'; + +import model from './model'; + +interface CustomProp { + title: string; +} + +interface MapModelToComponentProp { + model: ExtractIModelFromModelConfig; +} + +type ComponentProps = Assign; + +class Component extends React.Component{ + render() { + const { model, title } = this.props; + const [ state ] = model; + return ( + + ); + } +} + +interface MapModelToProp { + model: ExtractIModelAPIsFromModelConfig; +} + +type Props = Assign; + +export default withModel(model)(function ({ model, ...otherProps }) { + const ComponentWithModel = model.withValue()(Component); + return ; +}); diff --git a/examples/classComponent/src/components/Product/Function.tsx b/examples/classComponent/src/components/Product/Function.tsx new file mode 100644 index 00000000..4dd81746 --- /dev/null +++ b/examples/classComponent/src/components/Product/Function.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Assign } from 'utility-types'; +import { withModel, ExtractIModelAPIsFromModelConfig } from '@ice/store'; +import Product from './Product'; +import model from './model'; + +interface MapModelToProp { + model: ExtractIModelAPIsFromModelConfig; +} + +interface CustomProp { + title: string; +} + +type Props = Assign; + +function Component({ model, title }: Props) { + const [product] = model.useValue(); + return ( + + ); +} + +export default withModel(model)(Component); diff --git a/examples/classComponent/src/components/Product/Product.tsx b/examples/classComponent/src/components/Product/Product.tsx new file mode 100644 index 00000000..aec37ab5 --- /dev/null +++ b/examples/classComponent/src/components/Product/Product.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export default function({ productTitle, title, type }) { + return ( +
+
+
+ Component Type is: {type} +
+

{title}: {productTitle}

+
+ ); +} diff --git a/examples/classComponent/src/components/Product/index.ts b/examples/classComponent/src/components/Product/index.ts new file mode 100644 index 00000000..d7079a13 --- /dev/null +++ b/examples/classComponent/src/components/Product/index.ts @@ -0,0 +1,4 @@ +// import Component from './Function'; +import Component from './Class'; + +export default Component; diff --git a/examples/classComponent/src/components/Product/model.ts b/examples/classComponent/src/components/Product/model.ts new file mode 100644 index 00000000..d1946a88 --- /dev/null +++ b/examples/classComponent/src/components/Product/model.ts @@ -0,0 +1,5 @@ +export default { + state: { + title: 'foo', + }, +}; diff --git a/examples/classComponent/src/components/User/Class.tsx b/examples/classComponent/src/components/User/Class.tsx new file mode 100644 index 00000000..625bb065 --- /dev/null +++ b/examples/classComponent/src/components/User/Class.tsx @@ -0,0 +1,36 @@ +/* eslint-disable react/prefer-stateless-function */ +import React from 'react'; +import { Assign } from 'utility-types'; +import { ExtractIModelFromModelConfig } from '@ice/store'; +import store from '../../store'; +import User from './User'; +import userModel from '../../models/user'; + +const { withModel } = store; + +interface PropsWithModel { + user: ExtractIModelFromModelConfig; +} + +interface CustomProp { + title: string; +} + +type Props = Assign; + +class Component extends React.Component { + render() { + const { title, user } = this.props; + const [ state ] = user; + const { name } = state; + return User({ + name, + title, + type: 'Class', + }); + } +} + +export default withModel('user')( + Component, +); diff --git a/examples/classComponent/src/components/User/Function.tsx b/examples/classComponent/src/components/User/Function.tsx new file mode 100644 index 00000000..ed9a6e04 --- /dev/null +++ b/examples/classComponent/src/components/User/Function.tsx @@ -0,0 +1,15 @@ +import store from '../../store'; +import User from './User'; + +const { useModel } = store; + +export default function({ title }) { + const [ state ] = useModel('user'); + return User( + { + type: 'Function', + name: state.name, + title, + }, + ); +} diff --git a/examples/classComponent/src/components/User/User.tsx b/examples/classComponent/src/components/User/User.tsx new file mode 100644 index 00000000..67cc4fc4 --- /dev/null +++ b/examples/classComponent/src/components/User/User.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +export default function({ name, title, type }) { + return ( +
+
+
+ Component Type is: {type} +
+

{title}: {name}

+
+ ); +} diff --git a/examples/classComponent/src/components/User/index.ts b/examples/classComponent/src/components/User/index.ts new file mode 100644 index 00000000..d7079a13 --- /dev/null +++ b/examples/classComponent/src/components/User/index.ts @@ -0,0 +1,4 @@ +// import Component from './Function'; +import Component from './Class'; + +export default Component; diff --git a/examples/classComponent/src/index.tsx b/examples/classComponent/src/index.tsx new file mode 100644 index 00000000..c3d18560 --- /dev/null +++ b/examples/classComponent/src/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import store from './store'; +import User from './components/User'; +import Product from './components/Product'; + +const { Provider } = store; + +function App() { + return ( + + + + + ); +} + +const rootElement = document.getElementById('root'); +ReactDOM.render(, rootElement); diff --git a/examples/classComponent/src/models/index.ts b/examples/classComponent/src/models/index.ts new file mode 100644 index 00000000..6aba21ac --- /dev/null +++ b/examples/classComponent/src/models/index.ts @@ -0,0 +1,11 @@ +import { Models } from '@ice/store'; +import user from './user'; + +const rootModels: RootModels = { user }; + +// add interface to avoid recursive type checking +export interface RootModels extends Models { + user: typeof user; +} + +export default rootModels; diff --git a/examples/classComponent/src/models/user.ts b/examples/classComponent/src/models/user.ts new file mode 100644 index 00000000..c74fcf9a --- /dev/null +++ b/examples/classComponent/src/models/user.ts @@ -0,0 +1,5 @@ +export default { + state: { + name: 'Icestore', + }, +}; diff --git a/examples/classComponent/src/react-app-env.d.ts b/examples/classComponent/src/react-app-env.d.ts new file mode 100644 index 00000000..30da8962 --- /dev/null +++ b/examples/classComponent/src/react-app-env.d.ts @@ -0,0 +1 @@ +// / diff --git a/examples/classComponent/src/store.ts b/examples/classComponent/src/store.ts new file mode 100644 index 00000000..d8d5d9ac --- /dev/null +++ b/examples/classComponent/src/store.ts @@ -0,0 +1,11 @@ + +import { createStore, IcestoreRootState, IcestoreDispatch } from '@ice/store'; +import models from './models'; + +const store = createStore(models); + +export default store; +export type Models = typeof models; +export type Store = typeof store; +export type RootDispatch = IcestoreDispatch; +export type RootState = IcestoreRootState; diff --git a/examples/classComponent/tsconfig.json b/examples/classComponent/tsconfig.json new file mode 100644 index 00000000..171592f7 --- /dev/null +++ b/examples/classComponent/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": [ + "src" + ] +} diff --git a/examples/counter/src/index.tsx b/examples/counter/src/index.tsx index 63830f49..de0a0689 100644 --- a/examples/counter/src/index.tsx +++ b/examples/counter/src/index.tsx @@ -12,7 +12,7 @@ const counter = { decrement:(prevState) => prevState - 1, }, effects: () => ({ - async decrementAsync() { + async asyncDecrement() { await delay(1000); this.decrement(); }, @@ -30,12 +30,12 @@ const store = createStore(models); const { useModel } = store; function Counter() { const [ count, dispatchers ] = useModel('counter'); - const { increment, decrementAsync } = dispatchers; + const { increment, asyncDecrement } = dispatchers; return (
{count} - +
); } diff --git a/examples/migration-redux-1/package.json b/examples/migration-redux-1/package.json new file mode 100644 index 00000000..97d07a18 --- /dev/null +++ b/examples/migration-redux-1/package.json @@ -0,0 +1,31 @@ +{ + "name": "icestore-migration-part-1", + "version": "1.0.0", + "description": "Using icestore with Redux only.", + "private": true, + "dependencies": { + "@ice/store": "^1.4.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-redux": "^7.2.0" + }, + "devDependencies": { + "react-scripts": "3.4.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/migration-redux-1/public/index.html b/examples/migration-redux-1/public/index.html new file mode 100644 index 00000000..a6140def --- /dev/null +++ b/examples/migration-redux-1/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + redux Migration: Part 1 + + + +
+ + + diff --git a/examples/migration-redux-1/src/App.js b/examples/migration-redux-1/src/App.js new file mode 100644 index 00000000..f5edda44 --- /dev/null +++ b/examples/migration-redux-1/src/App.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { incrementSharks } from './reducers/sharks'; +import { incrementDolphins } from './reducers/dolphins'; + +const Count = props => ( +
+
+
+

Sharks

+

{props.sharks}

+ +
+
+

Dolphins

+

{props.dolphins}

+ +
+
+

Using Redux

+
+); + +const mapState = state => ({ + sharks: state.sharks, + dolphins: state.dolphins, +}); + +const mapDispatch = dispatch => ({ + incrementSharks: () => dispatch(incrementSharks(1)), + incrementDolphins: () => dispatch(incrementDolphins(1)), +}); + +export default connect( + mapState, + mapDispatch, +)(Count); diff --git a/examples/migration-redux-1/src/index.js b/examples/migration-redux-1/src/index.js new file mode 100644 index 00000000..1c8da969 --- /dev/null +++ b/examples/migration-redux-1/src/index.js @@ -0,0 +1,28 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore } from '@ice/store'; +import { Provider } from 'react-redux'; + +import sharks from './reducers/sharks'; +import dolphins from './reducers/dolphins'; +import App from './App'; + +const store = createStore( + {}, + { + redux: { + reducers: { + sharks, + dolphins, + }, + }, + }, +); + +const Root = () => ( + + + +); + +ReactDOM.render(, document.querySelector('#root')); diff --git a/examples/migration-redux-1/src/reducers/dolphins.js b/examples/migration-redux-1/src/reducers/dolphins.js new file mode 100644 index 00000000..8f9ddccd --- /dev/null +++ b/examples/migration-redux-1/src/reducers/dolphins.js @@ -0,0 +1,15 @@ +const INCREMENT = 'dolphins/increment'; + +export const incrementDolphins = (payload) => ({ + type: INCREMENT, + payload, +}); + +export default (state = 0, action) => { + switch (action.type) { + case INCREMENT: + return state + action.payload; + default: + return state; + } +}; diff --git a/examples/migration-redux-1/src/reducers/sharks.js b/examples/migration-redux-1/src/reducers/sharks.js new file mode 100644 index 00000000..0b0c957a --- /dev/null +++ b/examples/migration-redux-1/src/reducers/sharks.js @@ -0,0 +1,15 @@ +const INCREMENT = 'sharks/increment'; + +export const incrementSharks = (payload) => ({ + type: INCREMENT, + payload, +}); + +export default (state = 0, action) => { + switch (action.type) { + case INCREMENT: + return state + action.payload; + default: + return state; + } +}; diff --git a/examples/migration-redux-2/package.json b/examples/migration-redux-2/package.json new file mode 100644 index 00000000..819a421f --- /dev/null +++ b/examples/migration-redux-2/package.json @@ -0,0 +1,31 @@ +{ + "name": "icestore-migration-part-2", + "version": "1.0.0", + "description": "Using icestore with Redux & icestore.", + "private": true, + "dependencies": { + "@ice/store": "^1.4.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-redux": "^7.2.0" + }, + "devDependencies": { + "react-scripts": "3.4.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/migration-redux-2/public/index.html b/examples/migration-redux-2/public/index.html new file mode 100644 index 00000000..a6140def --- /dev/null +++ b/examples/migration-redux-2/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + redux Migration: Part 1 + + + +
+ + + diff --git a/examples/migration-redux-2/src/App.js b/examples/migration-redux-2/src/App.js new file mode 100644 index 00000000..10978d8a --- /dev/null +++ b/examples/migration-redux-2/src/App.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { incrementDolphins } from './reducers/dolphins'; + +const Count = props => ( +
+
+
+

Sharks

+

{props.sharks}

+ +
+
+

Dolphins

+

{props.dolphins}

+ +
+
+

Mixing Redux & icestore

+
+); + +const mapState = state => ({ + sharks: state.sharks, + dolphins: state.dolphins, +}); + +const mapDispatch = dispatch => ({ + incrementSharks: () => dispatch.sharks.increment(1), + incrementDolphins: () => dispatch(incrementDolphins(1)), +}); + +export default connect( + mapState, + mapDispatch, +)(Count); diff --git a/examples/migration-redux-2/src/index.js b/examples/migration-redux-2/src/index.js new file mode 100644 index 00000000..c37fe3fc --- /dev/null +++ b/examples/migration-redux-2/src/index.js @@ -0,0 +1,27 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore } from '@ice/store'; +import { Provider } from 'react-redux'; + +import sharks from './models/sharks'; +import dolphins from './reducers/dolphins'; +import App from './App'; + +const store = createStore( + { sharks }, + { + redux: { + reducers: { + dolphins, + }, + }, + }, +); + +const Root = () => ( + + + +); + +ReactDOM.render(, document.querySelector('#root')); diff --git a/examples/migration-redux-2/src/models/sharks.js b/examples/migration-redux-2/src/models/sharks.js new file mode 100644 index 00000000..e54a0a57 --- /dev/null +++ b/examples/migration-redux-2/src/models/sharks.js @@ -0,0 +1,6 @@ +export default { + state: 0, + reducers: { + increment: (state, payload) => state + payload, + }, +}; diff --git a/examples/migration-redux-2/src/reducers/dolphins.js b/examples/migration-redux-2/src/reducers/dolphins.js new file mode 100644 index 00000000..8f9ddccd --- /dev/null +++ b/examples/migration-redux-2/src/reducers/dolphins.js @@ -0,0 +1,15 @@ +const INCREMENT = 'dolphins/increment'; + +export const incrementDolphins = (payload) => ({ + type: INCREMENT, + payload, +}); + +export default (state = 0, action) => { + switch (action.type) { + case INCREMENT: + return state + action.payload; + default: + return state; + } +}; diff --git a/examples/migration-redux-3/package.json b/examples/migration-redux-3/package.json new file mode 100644 index 00000000..41414807 --- /dev/null +++ b/examples/migration-redux-3/package.json @@ -0,0 +1,31 @@ +{ + "name": "icestore-migration-part-3", + "version": "1.0.0", + "description": "Using icestore with react-redux only.", + "private": true, + "dependencies": { + "@ice/store": "^1.4.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-redux": "^7.2.0" + }, + "devDependencies": { + "react-scripts": "3.4.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/migration-redux-3/public/index.html b/examples/migration-redux-3/public/index.html new file mode 100644 index 00000000..a6140def --- /dev/null +++ b/examples/migration-redux-3/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + redux Migration: Part 1 + + + +
+ + + diff --git a/examples/migration-redux-3/src/App.js b/examples/migration-redux-3/src/App.js new file mode 100644 index 00000000..1a4eb26d --- /dev/null +++ b/examples/migration-redux-3/src/App.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { connect, useSelector, useDispatch } from './redux'; +import store from './store'; + +const Count = props => { + const sharks = useSelector(state => state.sharks); + const dispatch = useDispatch(); + + return ( +
+
+
+

Sharks

+

{sharks}

+ +
+
+

Dolphins

+

{props.dolphins}

+ +
+
+

Using react-redux

+
+ ); +}; + +const mapState = state => ({ + dolphins: state.dolphins, +}); + +const mapDispatch = dispatch => ({ + incrementDolphins: () => dispatch.dolphins.increment(1), +}); + +export default connect( + mapState, + mapDispatch, + undefined, + { context: store.context }, +)(Count); diff --git a/examples/migration-redux-3/src/index.js b/examples/migration-redux-3/src/index.js new file mode 100644 index 00000000..3873c9e2 --- /dev/null +++ b/examples/migration-redux-3/src/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import store from './store'; + +import App from './App'; + +const Root = () => ( + + + +); + +ReactDOM.render(, document.querySelector('#root')); diff --git a/examples/migration-redux-3/src/models/dolphins.js b/examples/migration-redux-3/src/models/dolphins.js new file mode 100644 index 00000000..e54a0a57 --- /dev/null +++ b/examples/migration-redux-3/src/models/dolphins.js @@ -0,0 +1,6 @@ +export default { + state: 0, + reducers: { + increment: (state, payload) => state + payload, + }, +}; diff --git a/examples/migration-redux-3/src/models/sharks.js b/examples/migration-redux-3/src/models/sharks.js new file mode 100644 index 00000000..e54a0a57 --- /dev/null +++ b/examples/migration-redux-3/src/models/sharks.js @@ -0,0 +1,6 @@ +export default { + state: 0, + reducers: { + increment: (state, payload) => state + payload, + }, +}; diff --git a/examples/migration-redux-3/src/redux.js b/examples/migration-redux-3/src/redux.js new file mode 100644 index 00000000..008d0f2b --- /dev/null +++ b/examples/migration-redux-3/src/redux.js @@ -0,0 +1,6 @@ +import { connect as reduxConnect, createSelectorHook, createDispatchHook } from 'react-redux'; +import store from './store'; + +export const useSelector = createSelectorHook(store.context); +export const useDispatch = createDispatchHook(store.context); +export const connect = reduxConnect; diff --git a/examples/migration-redux-3/src/store.js b/examples/migration-redux-3/src/store.js new file mode 100644 index 00000000..fce35b97 --- /dev/null +++ b/examples/migration-redux-3/src/store.js @@ -0,0 +1,10 @@ +import { createStore } from '@ice/store'; + +import sharks from './models/sharks'; +import dolphins from './models/dolphins'; + +const store = createStore( + { sharks, dolphins }, +); + +export default store; diff --git a/examples/migration-redux-4/package.json b/examples/migration-redux-4/package.json new file mode 100644 index 00000000..c7dd7dd5 --- /dev/null +++ b/examples/migration-redux-4/package.json @@ -0,0 +1,31 @@ +{ + "name": "icestore-migration-part-4", + "version": "1.0.0", + "description": "Using icestore with react-redux & icestore.", + "private": true, + "dependencies": { + "@ice/store": "^1.4.0", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-redux": "^7.2.0" + }, + "devDependencies": { + "react-scripts": "3.4.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + } +} diff --git a/examples/migration-redux-4/public/index.html b/examples/migration-redux-4/public/index.html new file mode 100644 index 00000000..a6140def --- /dev/null +++ b/examples/migration-redux-4/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + redux Migration: Part 1 + + + +
+ + + diff --git a/examples/migration-redux-4/src/App.js b/examples/migration-redux-4/src/App.js new file mode 100644 index 00000000..ee723807 --- /dev/null +++ b/examples/migration-redux-4/src/App.js @@ -0,0 +1,45 @@ +import React from 'react'; +import { connect, useSelector /* useDispatch */ } from './redux'; +import store from './store'; + +const Count = props => { + const sharks = useSelector(state => state.sharks); + // const dispatch = useDispatch(); + // const sharks = store.useModelState('sharks'); + const dispatchers = store.useModelDispatchers('sharks'); + + return ( +
+
+
+

Sharks

+

{sharks}

+ +
+
+

Dolphins

+

{props.dolphins}

+ +
+
+

Using react-redux & icestore

+
+ ); +}; + +const mapState = state => ({ + dolphins: state.dolphins, +}); + +// const mapDispatch = dispatch => ({ +// incrementDolphins: () => dispatch.dolphins.increment(1) +// }); + +const WrapperedCount = connect( + mapState, + undefined, + undefined, + { context: store.context }, +)(Count); + +export default store.withModelDispatchers('dolphins')(WrapperedCount); diff --git a/examples/migration-redux-4/src/index.js b/examples/migration-redux-4/src/index.js new file mode 100644 index 00000000..3873c9e2 --- /dev/null +++ b/examples/migration-redux-4/src/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import store from './store'; + +import App from './App'; + +const Root = () => ( + + + +); + +ReactDOM.render(, document.querySelector('#root')); diff --git a/examples/migration-redux-4/src/models/dolphins.js b/examples/migration-redux-4/src/models/dolphins.js new file mode 100644 index 00000000..71c8a377 --- /dev/null +++ b/examples/migration-redux-4/src/models/dolphins.js @@ -0,0 +1,6 @@ +export default { + state: 0, + reducers: { + increment: (state) => state + 1, + }, +}; diff --git a/examples/migration-redux-4/src/models/sharks.js b/examples/migration-redux-4/src/models/sharks.js new file mode 100644 index 00000000..e54a0a57 --- /dev/null +++ b/examples/migration-redux-4/src/models/sharks.js @@ -0,0 +1,6 @@ +export default { + state: 0, + reducers: { + increment: (state, payload) => state + payload, + }, +}; diff --git a/examples/migration-redux-4/src/redux.js b/examples/migration-redux-4/src/redux.js new file mode 100644 index 00000000..008d0f2b --- /dev/null +++ b/examples/migration-redux-4/src/redux.js @@ -0,0 +1,6 @@ +import { connect as reduxConnect, createSelectorHook, createDispatchHook } from 'react-redux'; +import store from './store'; + +export const useSelector = createSelectorHook(store.context); +export const useDispatch = createDispatchHook(store.context); +export const connect = reduxConnect; diff --git a/examples/migration-redux-4/src/store.js b/examples/migration-redux-4/src/store.js new file mode 100644 index 00000000..fce35b97 --- /dev/null +++ b/examples/migration-redux-4/src/store.js @@ -0,0 +1,10 @@ +import { createStore } from '@ice/store'; + +import sharks from './models/sharks'; +import dolphins from './models/dolphins'; + +const store = createStore( + { sharks, dolphins }, +); + +export default store; diff --git a/examples/todos/package.json b/examples/todos/package.json index 8e1c5fd9..dcd3a83a 100644 --- a/examples/todos/package.json +++ b/examples/todos/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "@ice/store": "^1.3.4", + "@ice/store": "^1.4.0", "lodash": "^4.17.15", "react": "^16.8.6", "react-dom": "^16.8.6" @@ -33,4 +33,4 @@ "last 1 safari version" ] } -} \ No newline at end of file +} diff --git a/examples/todos/src/components/AddTodo.tsx b/examples/todos/src/components/AddTodo.tsx new file mode 100644 index 00000000..ebb8b057 --- /dev/null +++ b/examples/todos/src/components/AddTodo.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import store from '../store'; + +const { useModelDispatchers } = store; + +export default function() { + const { add } = useModelDispatchers('todos'); + let input; + + return ( +
+
{ + e.preventDefault(); + if (!input.value.trim()) { + return; + } + add({ text: input.value }); + input.value = ''; + }}> + input = node} /> + + +
+ ); +} diff --git a/examples/todos/src/components/Car.tsx b/examples/todos/src/components/Car.tsx deleted file mode 100644 index 522ad161..00000000 --- a/examples/todos/src/components/Car.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import store from '../store'; - -const { useModel } = store; - -export default function UserApp() { - const [ state ] = useModel('car'); - const { logo } = state; - - console.debug('Car rending...'); - return ( -
- {logo} -
- ); -} diff --git a/examples/todos/src/components/Footer.tsx b/examples/todos/src/components/Footer.tsx new file mode 100644 index 00000000..089d1722 --- /dev/null +++ b/examples/todos/src/components/Footer.tsx @@ -0,0 +1,33 @@ + +import React from 'react'; +import { VisibilityFilters } from '../models/visibilityFilter'; +import store from '../store'; + +const Link = ({ active, children, onClick }) => ( + +); + +export default function Footer() { + const [state, dispatchers] = store.useModel('visibilityFilter'); + return ( +
+ Show: + { + Object.keys(VisibilityFilters).map((key) => { + return ( dispatchers.setState(key)}> + {key.toLowerCase()} + ); + }) + } +
+ ); +} diff --git a/examples/todos/src/components/Todo.tsx b/examples/todos/src/components/Todo.tsx new file mode 100644 index 00000000..e45f3de9 --- /dev/null +++ b/examples/todos/src/components/Todo.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +export default function({ completed, text, onAsyncRemove, onRemove, onToggle, isLoading }) { + return ( +
  • + + + +
  • + ); +} diff --git a/examples/todos/src/components/TodoAdd.tsx b/examples/todos/src/components/TodoAdd.tsx deleted file mode 100644 index 5606eb5f..00000000 --- a/examples/todos/src/components/TodoAdd.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import store from '../store'; - -const { useModelDispatchers } = store; - -export default function TodoAdd() { - const { add } = useModelDispatchers('todos'); - - console.debug('TodoAdd rending...'); - return ( - { - if (event.keyCode === 13) { - add({ - name: event.currentTarget.value, - }); - event.currentTarget.value = ''; - } - }} - placeholder="Press Enter" - /> - ); -} diff --git a/examples/todos/src/components/TodoList.tsx b/examples/todos/src/components/TodoList.tsx index cd6ee532..cc7b8062 100644 --- a/examples/todos/src/components/TodoList.tsx +++ b/examples/todos/src/components/TodoList.tsx @@ -1,49 +1,53 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import store from '../store'; +import Todo from './Todo'; +import { VisibilityFilters } from '../models/visibilityFilter'; const { useModel, useModelEffectsLoading } = store; -export function TodoList({ state, dispatchers, effectsLoading }) { - const { title, subTitle, dataSource } = state; - const { toggle, remove } = dispatchers; +const getVisibleTodos = (todos, filter) => { + switch (filter) { + case VisibilityFilters.ALL: + return todos; + case VisibilityFilters.COMPLETED: + return todos.filter(t => t.completed); + case VisibilityFilters.ACTIVE: + return todos.filter(t => !t.completed); + default: + throw new Error(`Unknown filter: ${ filter}`); + } +}; - return ( -
    -

    {title}

    -

    - Now is using {subTitle}. -

    -
      - {dataSource.map(({ name, done = false }, index) => ( -
    • - - { - effectsLoading.remove ? - '...deleting...' : - - } -
    • - ))} -
    -
    - ); -} - -export default function({ title }) { - const [ state, dispatchers ] = useModel('todos'); +export default function TodoList() { + const [ todos, dispatchers ] = useModel('todos'); + const [ visibilityFilter ] = useModel('visibilityFilter'); const effectsLoading = useModelEffectsLoading('todos'); - return TodoList( - { - state: { ...state, title, subTitle: 'Function Component' }, - dispatchers, - effectsLoading, - }, - ); + + const { refresh, asyncRemove, remove, toggle } = dispatchers; + const visableTodos = getVisibleTodos(todos, visibilityFilter); + + useEffect(() => { + refresh(); + // eslint-disable-next-line + }, []); + + const noTaskView =
    No task
    ; + const loadingView =
    Loading...
    ; + const taskView = visableTodos.length ? ( +
      + {visableTodos.map(({ text, completed }, index) => ( + asyncRemove(index)} + onRemove={() => remove(index)} + onToggle={() => toggle(index)} + isLoading={effectsLoading.asyncRemove} + /> + ))} +
    + ) : noTaskView; + + return effectsLoading.refresh ? loadingView : taskView; } diff --git a/examples/todos/src/components/TodoListClass.tsx b/examples/todos/src/components/TodoListClass.tsx deleted file mode 100644 index 9e764e1f..00000000 --- a/examples/todos/src/components/TodoListClass.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Component } from 'react'; -import { Assign } from 'utility-types'; -import { ExtractIModelFromModelConfig, ExtractIModelEffectsLoadingFromModelConfig } from '@ice/store'; -// import compose from 'lodash/fp/compose'; -import store from '../store'; -import { TodoList as TodoListFn } from './TodoList'; -import todosModel from '../models/todos'; - -const { withModel, withModelEffectsLoading } = store; - -interface MapModelToProp { - todos: ExtractIModelFromModelConfig; -} - -interface MapModelEffectsStateToProp { - todosEffectsLoading: ExtractIModelEffectsLoadingFromModelConfig; -} - -interface CustomProp { - title: string; -} - -type PropsWithModel = Assign; -type Props = Assign; - -class TodoList extends Component { - onRemove = (index) => { - const [, dispatchers] = this.props.todos; - dispatchers.remove(index); - } - - onToggle = (index) => { - const [, dispatchers] = this.props.todos; - dispatchers.toggle(index); - } - - render() { - const { title, todos, todosEffectsLoading } = this.props; - const [ state ] = todos; - const { dataSource } = state; - return TodoListFn({ - state: { title, dataSource, subTitle: 'Class Component' }, - dispatchers: { toggle: this.onToggle, remove: this.onRemove }, - effectsLoading: todosEffectsLoading, - }); - } -} - -export default withModelEffectsLoading('todos')( - withModel('todos')(TodoList), -); - -// functional flavor: -// export default compose(withModelEffectsLoading('todos'), withModel('todos'))(TodoList); diff --git a/examples/todos/src/components/Todos.tsx b/examples/todos/src/components/Todos.tsx deleted file mode 100644 index 56cdbd16..00000000 --- a/examples/todos/src/components/Todos.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { useEffect } from 'react'; -import store from '../store'; -// import TodoList from './TodoListClass'; -import TodoList from './TodoList'; - -const { useModel, useModelEffectsLoading } = store; - -export default function Todos() { - const todos = useModel('todos'); - const [ state, dispatchers ] = todos; - const effectsLoading = useModelEffectsLoading('todos'); - - const { dataSource } = state; - const { refresh } = dispatchers; - - useEffect(() => { - refresh(); - - // eslint-disable-next-line - }, []); - - const noTaskView =
    no task
    ; - const loadingView =
    loading...
    ; - const taskView = dataSource.length ? : noTaskView; - - console.debug('Todos rending... '); - return effectsLoading.refresh ? loadingView : taskView; -} diff --git a/examples/todos/src/components/User.tsx b/examples/todos/src/components/User.tsx deleted file mode 100644 index 57ea7ca9..00000000 --- a/examples/todos/src/components/User.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useEffect } from 'react'; -import store from '../store'; - -const { useModel } = store; - -export default function UserApp() { - const [ state, dispatchers ] = useModel('user'); - const { dataSource, auth, todos } = state; - const { login } = dispatchers; - const { name } = dataSource; - - useEffect(() => { - login(); - - // eslint-disable-next-line - }, []); - - console.debug('UserApp rending...'); - return auth ? - (
    -

    - User Information -

    -
      -
    • Name:{name}
    • -
    • Todos:{todos}
    • -
    -
    ) : - (
    - Not logged in -
    ); -} diff --git a/examples/todos/src/index.tsx b/examples/todos/src/index.tsx index c5787702..d88b2d52 100644 --- a/examples/todos/src/index.tsx +++ b/examples/todos/src/index.tsx @@ -1,30 +1,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; import store from './store'; -import Todos from './components/Todos'; -import TodoAdd from './components/TodoAdd'; -import User from './components/User'; -import Car from './components/Car'; - -const initialStates = { - user: { - dataSource: { - name: 'Tom', - }, - auth: true, - todos: 5, - }, -}; +import TodoList from './components/TodoList'; +import AddTodo from './components/AddTodo'; +import Footer from './components/Footer'; const { Provider } = store; function App() { return ( - - - - - + + + +