diff --git a/README.md b/README.md index 4d9564e9..270f5011 100644 --- a/README.md +++ b/README.md @@ -1,350 +1,110 @@ -English | [简体中文](./README.zh-CN.md) - # icestore -> Lightweight React state management library based on react hooks +> Lightweight state management solution based on React Hooks. [![NPM version](https://img.shields.io/npm/v/@ice/store.svg?style=flat)](https://npmjs.org/package/@ice/store) [![Package Quality](https://npm.packagequality.com/shield/@ice%2Fstore.svg)](https://packagequality.com/#?package=@ice/store) [![build status](https://img.shields.io/travis/ice-lab/icestore.svg?style=flat-square)](https://travis-ci.org/ice-lab/icestore) -[![Test coverage](https://img.shields.io/codecov/c/github/ice-lab/icestore.svg?style=flat-square)](https://codecov.io/gh/ice-lab/icestore) [![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) -## Installation - -icestore requires React 16.8.0 or later. - -```bash -npm install @ice/store --save -``` + + + + + + + + + + + + +
🕹 CodeSandbox demos 🕹
CounterTodos
## Introduction icestore is a lightweight React state management library based on hooks. It has the following core features: -* **Minimal & Familiar API**: No additional learning costs, easy to get started with the knowledge of React Hooks. -* **Class Component Support**: Make old projects enjoying the fun of lightweight state management with friendly compatibility strategy. +* **Minimal & Familiar API**: No additional learning costs, easy to get started with the knowledge of React Hooks, friendly to Redux users. * **Built in Async Status**: Records loading and error status of async actions, simplifying the rendering logic in the view layer. +* **Class Component Support**: Make old projects enjoying the fun of lightweight state management with friendly compatibility strategy. * **TypeScript Support**: Provide complete type definitions to support intelliSense in VS Code. -## Getting Started - -Let's build a simple todo app from scatch using icestore which includes following steps: +See the [comparison table](docs/recipes.md#Comparison) for more details. -### Step 1 - Use a model to define your store: - -```javascript -export const todos = { - state: { - dataSource: [], - }, - actions: { - async fetch(prevState, actions) { - await delay(1000); - const dataSource = [ - { name: 'react' }, - { name: 'vue', done: true}, - { name: 'angular' }, - ]; - return { - ...prevState, - dataSource, - } - }, - add(prevState, todo) { - return { - ...prevState, - dataSource: [ - ...prevState.dataSource, - todo, - ] - }; - }, - }, -}; -``` - -### Step 2 - Create the store - -```javascript -import { createStore } from '@ice/store'; -import * as models from './models'; - -export default createStore(models); -``` - -### Step 3 - Wrap your application +## Basic example ```jsx import React from 'react'; import ReactDOM from 'react-dom'; -import store from './store'; - -const { Provider } = store; - -ReactDOM.render( - - - , - rootEl -); -``` - -### Step 4 - Consume model - -```jsx -import React, { useEffect } from 'react'; -import store from './store'; - -const { useModel } = store; - -function Todos() { - const [ state, actions ] = useModel('todos'); - const { dataSource } = state; - const { fetch, add } = actions; - - useEffect(() => { - fetch(); - }, []); - - function onAdd(event, name) { - if (event.keyCode === 13) { - add({ name: event.target.value }); - event.target.value = ''; - } - } - - return ( -
- -
- -
-
- ); -} -``` - -## API - -**createStore** - -`createStore(models)` - -The function called to create a store. - -```js import { createStore } from '@ice/store'; -const store = createStore(models); -const { Provider, useModel, withModel } = store; -``` - -### Parameters - -**models** - -```js -import { createStore } from '@ice/store' - -const counterModel = { - state: { - value: 0 - }, -}; - -const models = { - counter: counterModel -}; - -createStore(models) -``` - -#### state - -`state: any`: Required - -The initial state of the model. - -```js -const model = { - state: { loading: false }, -}; -``` - -#### actions - -`actions: { [string]: (prevState, payload, actions, globalActions) => any }` +const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time)); -An object of functions that change the model's state. These functions take the model's previous state and a payload, and return the model's next state. - -```js +// 1️⃣ Use a model to define your store const counter = { state: 0, actions: { - add: (prevState, payload) => prevState + payload, - } -}; -``` - -Actions provide a simple way of handling async actions when used with async/await: - -```js -const counter = { - actions: { - async addAsync(prevState, payload) => { + increment:(prevState) => prevState + 1, + async decrement(prevState) { await delay(1000); - return prevState + payload; - }, - } -}; -``` - -You can call another action by useing `actions` or `globalActions`: - -```js -const user = { - state: { - foo: [], - } - actions: { - like(prevState, payload, actions, globalActions) => { - actions.foo(payload); // call user's actions - globalActions.user.foo(payload); // call actions of another model - - // do something... - - return { - ...prevState, - }; - }, - foo(prevState, id) { - // do something... - - return { - ...prevState, - }; + return prevState - 1; }, - } -}; -``` - -### Return - -#### Provider - -`Provider(props: { children, initialStates })` - -Exposes the store to your React application, so that your components will be able to consume and interact with the store via the hooks. - -```jsx -import React from 'react'; -import ReactDOM from 'react-dom'; -import { createStore } from '@ice/store'; - -const { Provider } = createStore(models); -ReactDOM.render( - - - , - rootEl -); -``` - -#### useModel - -`useModel(name: string): [ state, actions ]` - -A hook granting your components access to the model instance. - -```jsx -const counter = { - state: { - value: 0 }, - actions: { - add: (prevState, payload) => ({...prevState, value: prevState.value + payload}), - } }; -const { userModel } = createStore({ counter }); - -function FunctionComponent() { - const [ state, actions ] = userModel('name'); +const models = { + counter, +}; - state.value; // 0 +// 2️⃣ Create the store +const store = createStore(models); - actions.add(1); // state.value === 1 +// 3️⃣ Consume model +const { useModel } = store; +function Counter() { + const [ count, actions ] = useModel('counter'); + const { increment, decrement } = actions; + return ( +
+ {count} + + +
+ ); } -``` -#### useModelActions - -`useModelActions(name: string): actions` - -A hook granting your components access to the model actions. - -```js -function FunctionComponent() { - const actions = useModelActions('name'); - actions.add(1); +// 4️⃣ Wrap your components with Provider +const { Provider } = store; +function App() { + return ( + + + + ); } -``` - -#### useModelActionsState - -`useModelActionsState(name: string): { [actionName: string]: { isLoading: boolean, error: Error } } ` -A hook granting your components access to the action state of the model. +const rootElement = document.getElementById('root'); +ReactDOM.render(, rootElement); +``` -```js -function FunctionComponent() { - const actions = useModelActions('name'); - const actionsState = useModelActionsState('name'); +## Installation - useEffect(() => { - actions.fetch(); - }, []); +icestore requires React 16.8.0 or later. - actionsState.fetch.isLoading; - actionsState.fetch.error; -} +```bash +npm install @ice/store --save ``` -#### withModel +## API -`withModel(name: string, mapModelToProps?: (model: [state, actions]) => Object = (model) => ({ [name]: model }) ): (React.Component) => React.Component` +[docs/api](./docs/api.md) -Use withModel to connect the model and class component: +## Recipes -```jsx -class TodoList extends Component { - render() { - const { counter } = this.props; - const [ state, actions ] = counter; - const { dataSource } = state; - - state.value; // 0 - - actions.add(1); - } -} - -export default withModel('counter')(TodoList); -``` +[docs/recipes](./docs/recipes.md) ## Browser Compatibility @@ -362,6 +122,15 @@ Feel free to report any questions as an [issue](https://github.com/alibaba/ice/i If you're interested in icestore, see [CONTRIBUTING.md](https://github.com/alibaba/ice/blob/master/.github/CONTRIBUTING.md) for more information to learn how to get started. +## Community + +| DingTalk community | GitHub issues | Gitter | +|-------------------------------------|--------------|---------| +| | [issues] | [gitter]| + +[issues]: https://github.com/alibaba/ice/issues +[gitter]: https://gitter.im/alibaba/ice + ## License [MIT](LICENSE) diff --git a/README.zh-CN.md b/README.zh-CN.md deleted file mode 100644 index be626a2a..00000000 --- a/README.zh-CN.md +++ /dev/null @@ -1,366 +0,0 @@ -[English](./README.md) | 简体中文 - -# icestore - -> 基于 React Hooks 实现的轻量级状态管理框架 - -[![NPM version](https://img.shields.io/npm/v/@ice/store.svg?style=flat)](https://npmjs.org/package/@ice/store) -[![Package Quality](https://npm.packagequality.com/shield/@ice%2Fstore.svg)](https://packagequality.com/#?package=@ice/store) -[![build status](https://img.shields.io/travis/ice-lab/icestore.svg?style=flat-square)](https://travis-ci.org/ice-lab/icestore) -[![Test coverage](https://img.shields.io/codecov/c/github/ice-lab/icestore.svg?style=flat-square)](https://codecov.io/gh/ice-lab/icestore) -[![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) - -## 安装 - -使用 icestore 需要 React 在 16.8.0 版本以上。 - -```bash -$ npm install @ice/store --save -``` - -## 简介 - -icestore 是基于 React Hooks 实现的轻量级状态管理框架,具有以下特征: - -* **简单、熟悉的 API**:不需要额外的学习成本,只需要声明模型,然后 `useModel`; -* **支持组件 Class 写法**:友好的兼容策略可以让老项目享受轻量状态管理的乐趣; -* **集成异步处理**:记录异步操作时的执行状态,简化视图中对于等待或错误的处理逻辑; -* **良好的 TypeScript 支持**:提供完整的 TypeScript 类型定义,在 VS Code 中能获得完整的类型检查和推断。 - -## 快速开始 - -让我们使用 icestore 开发一个简单的 Todo 应用,包含以下几个步骤: - -### 第一步:定义模型 - -```javascript -export const todos = { - state: { - dataSource: [], - }, - actions: { - async fetch(prevState, actions) { - await delay(1000); - const dataSource = [ - { name: 'react' }, - { name: 'vue', done: true}, - { name: 'angular' }, - ]; - return { - ...prevState, - dataSource, - } - }, - add(prevState, todo) { - return { - ...prevState, - dataSource: [ - ...prevState.dataSource, - todo, - ] - }; - }, - }, -}; -``` - -### 第二步:创建 Store - -```javascript -import { createStore } from '@ice/store'; -import * as models from './models'; - -export default createStore(models); -``` - -### 第三步:挂载 Store - -```jsx -import React from 'react'; -import ReactDOM from 'react-dom'; -import store from './store'; - -const { Provider } = store; - -ReactDOM.render( - - - , - rootEl -); -``` - -### 第四步:消费模型 - -```jsx -import React, { useEffect } from 'react'; -import store from './store'; - -const { useModel } = store; - -function Todos() { - const [ state, actions ] = useModel('todos'); - const { dataSource } = state; - const { fetch, add } = actions; - - useEffect(() => { - fetch(); - }, []); - - function onAdd(event, name) { - if (event.keyCode === 13) { - add({ name: event.target.value }); - event.target.value = ''; - } - } - - return ( -
- -
- -
-
- ); -} -``` - -## API - -**createStore** - -`createStore(models)` - -该函数用于创建 Store,将返回一个 Provider 和一些 Hooks。 - -```js -import { createStore } from '@ice/store'; - -const store = createStore(models); -const { Provider, useModel, withModel } = store; -``` - -### 入参 - -**models** - -```js -import { createStore } from '@ice/store' - -const counterModel = { - state: { - value: 0 - }, -}; - -const models = { - counter: counterModel -}; - -createStore(models) -``` - -#### state - -`state: any`:必填 - -该模型的初始 state。 - -```js -const model = { - state: { loading: false }, -}; -``` - -#### actions - -`actions: { [string]: (prevState, payload, actions, globalActions) => any }` - -一个改变该模型 state 的所有函数的对象。这些函数采用模型的上一次 state 和一个 payload 作为形参,并且返回模型的下一个状态。 - -```js -const counter = { - state: 0, - actions: { - add: (prevState, payload) => prevState + payload, - } -}; -``` - -action 可以是异步的: - -```js -const counter = { - actions: { - async addAsync(prevState, payload) => { - await delay(1000); - return prevState + payload; - }, - } -}; -``` - -可以在返回前执行另一个 action 或者另一个模型的 actions: - -```js -const user = { - state: { - foo: [], - } - actions: { - like(prevState, payload, actions, globalActions) => { - actions.foo(payload); // 调用本模型的 foo - globalActions.user.foo(payload); // 调用其他模型的 foo - - // 做一些操作 - - return { - ...prevState, - }; - }, - foo(prevState, id) { - // 做一些操作 - - return { - ...prevState, - }; - }, - } -}; -``` - -### 返回值 - -#### Provider - -`Provider(props: { children, initialStates })` - -将 Store 挂载到 React 应用,以便组件能够通过 Hooks 使用 Store 并与 Store 进行交互。 - -```jsx -import React from 'react'; -import ReactDOM from 'react-dom'; -import { createStore } from '@ice/store'; - -const { Provider } = createStore(models); -ReactDOM.render( - - - , - rootEl -); -``` - -#### useModel - -`useModel(name: string): [ state, actions ]` - -在组件内使用模型实例。 - -```jsx -const counter = { - state: { - value: 0 - }, - actions: { - add: (prevState, payload) => ({...prevState, value: prevState.value + payload}), - } -}; - -const { userModel } = createStore({ counter }); - -function FunctionComponent() { - const [ state, actions ] = userModel('name'); - - state.value; // 0 - - actions.add(1); // state.value === 1 -} -``` - -#### useModelActions - -`useModelActions(name: string): actions` - -useModelActions 提供了一种只使用模型的 actions 但不订阅模型更新的的方式。 - -```js -function FunctionComponent() { - const actions = useModelActions('name'); - actions.add(1); -} -``` - -#### useModelActionsState - -`useModelActionsState(name: string): { [actionName: string]: { isLoading: boolean, error: Error } } ` - -使用 useModelActionsState 来获取模型异步 Action 的执行状态。 - -```js -function FunctionComponent() { - const actions = useModelActions('name'); - const actionsState = useModelActionsState('name'); - - useEffect(() => { - actions.fetch(); - }, []); - - actionsState.fetch.isLoading // 异步 Action 是否在执行中 - actionsState.fetch.error // 异步 Action 执行是否有误,注意仅当 isLoading 为 false 时这个值才有意义 -} -``` - -#### withModel - -`withModel(name: string, mapModelToProps?: (model: [state, actions]) => Object = (model) => ({ [name]: model }) ): (React.Component) => React.Component` - -使用 withModel 来连接模型和 Class Component。 - -```jsx -class TodoList extends Component { - render() { - const { counter } = this.props; - const [ state, actions ] = counter; - const { dataSource } = state; - - state.value; // 0 - - actions.add(1); - } -} - -export default withModel('counter')(TodoList); -``` - -## 浏览器支持 - -| ![Chrome](https://raw.github.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png) | ![Firefox](https://raw.github.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png) | ![Edge](https://raw.github.com/alrra/browser-logos/master/src/edge/edge_48x48.png) | ![IE](https://raw.github.com/alrra/browser-logos/master/src/archive/internet-explorer_9-11/internet-explorer_9-11_48x48.png) | ![Safari](https://raw.github.com/alrra/browser-logos/master/src/safari/safari_48x48.png) | ![Opera](https://raw.github.com/alrra/browser-logos/master/src/opera/opera_48x48.png) | ![UC](https://raw.github.com/alrra/browser-logos/master/src/uc/uc_48x48.png) | -| :--------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------: | :--------------------------------------------------------------------------: | -|✔ |✔|✔|9+ ✔|✔|✔|✔| - -## 灵感 - -创造 icestore 的灵感来源于 [constate](https://github.com/diegohaz/constate) 和 [rematch](https://github.com/rematch/rematch)。 - -## 贡献 - -欢迎[反馈问题](https://github.com/alibaba/ice/issues/new)。如果对 icestore 感兴趣,请参考 [CONTRIBUTING.md](https://github.com/alibaba/ice/blob/master/.github/CONTRIBUTING.md) 学习如何贡献代码。 - -## 协议 - -[MIT](LICENSE) - diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000..61a15e7d --- /dev/null +++ b/docs/api.md @@ -0,0 +1,460 @@ +--- +id: api +title: API +--- + +The `createStore` is a main function exported from the library, which creates a provider and other hooks. + +## createStore + +`createStore(models)` + +The function called to create a store. + +```js +import { createStore } from '@ice/store'; + +const { + Provider, + useModel, + useModelActions, + useModelActionsState, + withModel, + withModelActions, + withModelActionsState, +} = createStore(models); +``` + +### models + +```js +import { createStore } from '@ice/store' + +const counterModel = { + state: { + value: 0, + }, +}; + +const models = { + counter: counterModel, +}; + +createStore(models); +``` + +#### state + +`state: any`: Required + +The initial state of the model. + +```js +const model = { + state: { loading: false }, +}; +``` + +#### actions + +`actions: { [string]: (prevState, payload, actions, globalActions) => any }` + +An object of functions that change the model's state. These functions take the model's previous state and a payload, and return the model's next state. + +```js +const counter = { + state: 0, + actions: { + add: (prevState, payload) => prevState + payload, + }, +}; +``` + +Actions provide a simple way of handling async actions when used with async/await: + +```js +const counter = { + actions: { + async addAsync(prevState, payload) => { + await delay(1000); + return prevState + payload; + }, + }, +}; +``` + +You can call another action by useing `actions` or `globalActions`: + +```js +const user = { + state: { + foo: [], + }, + actions: { + like(prevState, payload, actions, globalActions) => { + actions.foo(payload); // call user's actions + globalActions.user.foo(payload); // call actions of another model + + // do something... + + return { + ...prevState, + }; + }, + foo(prevState, id) { + // do something... + + return { + ...prevState, + }; + }, + }, +}; +``` + +### Provider + +`Provider(props: { children, initialStates })` + +Exposes the store to your React application, so that your components will be able to consume and interact with the store via the hooks. + +```jsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore } from '@ice/store'; + +const { Provider } = createStore(models); +ReactDOM.render( + + + , + rootEl +); +``` + +Set initialStates: + +```jsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore } from '@ice/store'; + +const models = { + todo: { + state: {}, + }, + user: { + state: {}, + } +}; +const { Provider } = createStore(models); + +const initialStates = { + todo: { + title: 'Foo', + done: true, + }, + user: { + name: 'Alvin', + age: 18, + }, +}; + +ReactDOM.render( + + + , + rootEl +); +``` + +### useModel + +`useModel(name: string): [ state, actions ]` + +A hook granting your components access to the model instance. + +```jsx +const counter = { + state: { + value: 0, + }, + actions: { + add: (prevState, payload) => ({...prevState, value: prevState.value + payload}), + }, +}; + +const { userModel } = createStore({ counter }); + +function FunctionComponent() { + const [ state, actions ] = userModel('name'); + + state.value; // 0 + + actions.add(1); // state.value === 1 +} +``` + +### useModelActions + +`useModelActions(name: string): actions` + +A hook granting your components access to the model actions. + +```js +function FunctionComponent() { + const actions = useModelActions('name'); + actions.add(1); +} +``` + +### useModelActionsState + +`useModelActionsState(name: string): { [actionName: string]: { isLoading: boolean, error: Error } } ` + +A hook granting your components access to the action state of the model. + +```js +function FunctionComponent() { + const actions = useModelActions('foo'); + const actionsState = useModelActionsState('foo'); + + useEffect(() => { + actions.fetch(); + }, []); + + actionsState.fetch.isLoading; + actionsState.fetch.error; +} +``` + +### withModel + +`withModel(name: string, mapModelToProps?: (model: [state, actions]) => Object = (model) => ({ [name]: model }) ): (React.Component) => React.Component` + +Use withModel to connect the model and class component: + +```jsx +import { UseModelValue } from '@ice/store'; +import todosModel from '@/models/todos'; +import store from '@/store'; + +interface Props { + todos: UseModelValue; // `withModel` automatically adds the name of the model as the property +} + +class TodoList extends Component { + render() { + const { counter } = this.props; + const [ state, actions ] = counter; + + state.value; // 0 + + actions.add(1); + } +} + +export default withModel('counter')(TodoList); +``` + +use `mapModelToProps` to set the property: + +```tsx +import { UseModelValue } from '@ice/store'; +import todosModel from '@/models/todos'; +import store from '@/store'; + +const { withModel } = store; + +interface Props { + title: string; + customKey: UseModelValue; +} + +class TodoList extends Component { + render() { + const { title, customKey } = this.props; + const [ state, actions ] = customKey; + + state.field; // get state + actions.add({ /* ... */}); // run action + } +} + +export default withModel( + 'todos', + + // mapModelToProps: (model: [state, actions]) => Object = (model) => ({ [modelName]: model }) ) + (model) => ({ + customKey: model, + }) +)(TodoList); +``` + +### withModelActions + +`withModelActions(name: string, mapModelActionsToProps?: (actions) => Object = (actions) => ({ [name]: actions }) ): (React.Component) => React.Component` + +```tsx +import { ModelActionsState, ModelActions } from '@ice/store'; +import todosModel from '@/models/todos'; +import store from '@/store'; + +const { withModelActions } = store; + +interface Props { + todosActions: ModelActions; // `withModelActions` automatically adds `${modelName}Actions` as the property +} + +class TodoList extends Component { + render() { + const { todosActions } = this.props; + + todosActions.add({ /* ... */}); // run action + } +} + +export default withModelActions('todos')(TodoList); +``` + +You can use `mapModelActionsToProps` to set the property as the same way like `mapModelToProps`. + +### withModelActionsState + +`withModelActionsState(name: string, mapModelActionsStateToProps?: (actionsState) => Object = (actionsState) => ({ [name]: actionsState }) ): (React.Component) => React.Component` + +```tsx +import { ModelActionsState, ModelActions } from '@ice/store'; +import todosModel from '@/models/todos'; +import store from '@/store'; + +const { withModelActionsState } = store; + +interface Props { + todosActionsState: ModelActionsState; // `todosActionsState` automatically adds `${modelName}ActionsState` as the property +} + +class TodoList extends Component { + render() { + const { todosActionsState } = this.props; + + todosActionsState.add.isLoading; // get action state + } +} + +export default withModelActionsState('todos')(TodoList); +``` + +You can use `mapModelActionsStateToProps` to set the property as the same way like `mapModelToProps`. + +## createModel + +`createStore(model)` + +The function called to create a model. + + +```js +import { createModel } from '@ice/store'; + +const [ + Provider, + useState, + useActions, + useActionsState, +] = createModel(model); +``` + +### Provider + +`Provider(props: { children, initialState })` + +Exposes the model to your React application, so that your components will be able to consume and interact with the model via the hooks. + +```jsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore } from '@ice/store'; + +const { Provider } = createModel(model); +ReactDOM.render( + + + , + rootEl +); +``` + +Set initialState: + +```jsx +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore } from '@ice/store'; + +const userModel = { + state: {}, +}; +const { Provider } = createModel(userModel); +ReactDOM.render( + + + , + rootEl +); +``` + +#### useState + +`useState(): state` + +A hook granting your components access to the model state. + +```jsx +const counter = { + state: { + value: 0, + }, +}; + +const [, useState] = createModel(counter); + +function FunctionComponent() { + const state = useState(); + + state.value; // 0 +} +``` + +### useActions + +`useActions(): actions` + +A hook granting your components access to the model actions. + +```js +function FunctionComponent() { + const actions = useActions(); + actions.add(1); +} +``` + +### useActionsState + +`useActionsState(): { [actionName: string]: { isLoading: boolean, error: Error } } ` + +A hook granting your components access to the action state of the model. + +```js +function FunctionComponent() { + const actions = useActions(); + const actionsState = useActionsState(); + + useEffect(() => { + actions.fetch(); + }, []); + + actionsState.fetch.isLoading; + actionsState.fetch.error; +} +``` diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 00000000..54e04438 --- /dev/null +++ b/docs/recipes.md @@ -0,0 +1,206 @@ +--- +id: recipes +title: Recipes +--- + +## Model interaction + +Model interaction is a common usage scene which can be implemented by calling actions from other model in a model's action. + +### Example + +Suppose you have a User Model, which records the number of tasks of the user. And a Tasks Model, which records the task list of the system. Every time a user adds a task, user's task number needs to be updated. + +```tsx +// src/models/user +export default { + state: { + name: '', + tasks: 0, + }, + actions: { + async refresh() { + return await fetch('/user'); + }, + }, +}; + +// src/models/tasks +import { user } from './user'; + +export default { + state: [], + actions: { + async refresh() { + return await fetch('/tasks'); + }, + async add(prevState, task, actions, globalActions) { + await fetch('/tasks/add', task); + + // Retrieve user information after adding tasks + await globalActions.user.refresh(); + + // Retrieve todos after adding tasks + await actions.refresh(); + + return { ...prevState }; + }, + } +}; + +// src/store +import { createStore } from '@ice/store'; +import task from './model/task'; +import user from './model/user'; + +export default createStore({ + task, + user, +}); +``` + +### Pay attention to circular dependencies + +Please pay attention to circular dependencies problem when actions calling each other between models. + +For example, the action A in Model A calls the action B in Model B and the action B in Model B calls the action A in Model A will results into an endless loop. + +Be careful the possibility of endless loop problem will arise when methods from different models call each other. + +## Async actions' executing status + +`icestore` has built-in support to access the executing status of async actions. This enables users to have access to the isLoading and error executing status of async actions without defining extra state, making the code more consise and clean. + +### Example + +```js +import { useModelActions } from './store'; + +function FunctionComponent() { + const actions = useModelActions('name'); + const actionsState = useModelActionsState('name'); + + useEffect(() => { + actions.fetch(); + }, []); + + actionsState.fetch.isLoading; + actionsState.fetch.error; +} +``` + +## Class Component Support + +You can also using icestore with Class Component. The `withModel()` function connects a Model to a React component. + +### Basic + +```tsx +import { UseModelValue } from '@ice/store'; +import todosModel from '@/models/todos'; +import store from '@/store'; + +const { withModel } = store; + +interface MapModelToProp { + todos: UseModelValue; // `withModel` automatically adds the name of the model as the property +} + +interface Props extends MapModelToProp { + title: string; // custom property +} + +class TodoList extends Component { + render() { + const { title, todos } = this.props; + const [ state, actions ] = todos; + + state.field; // get state + actions.add({ /* ... */}); // run action + } +} + +export default withModel('todos')(TodoList); +``` + +### With multiple models + +```tsx +import { UseModelValue } from '@ice/store'; +import todosModel from '@/models/todos'; +import userModel from '@/models/user'; +import store from '@/store'; + +const { withModel } = store; + +interface Props { + todos: UseModelValue; + user: UseModelValue; +} + +class TodoList extends Component { + render() { + const { todos, user } = this.props; + const [ todoState, todoActions ] = todos; + const [ userState, userActions ] = user; + } +} + +export default withModel('user')( + withModel('todos')(TodoList) +); + +// functional flavor: +import compose from 'lodash/fp/compose'; +export default compose(withModel('user'), withModel('todos'))(TodoList); +``` + +### withModelActions & withModelActionsState + +You can use `withModelActions` to call only model actions without listening for model changes, also for `withModelActionsState`. + +See [docs/api](./api.md) for more details. + +## Directory organization + +For most small and medium-sized projects, it is recommended to centrally manage all the project models in the global `src/models/` directory: + +```bash +├── src/ +│ ├── components/ +│ │ └── NotFound/ +│ ├── pages/ +│ │ └── Home +│ ├── models/ +│ │ ├── modelA.js +│ │ ├── modelB.js +│ │ ├── modelC.js +│ │ └── index.js +│ └── store.js +``` + +If the project is relatively large, or more likely to follow the page maintenance of the store,then you can declare a store instance in each page directory. However, in this case, cross page store calls should be avoided as much as possible. + +## Comparison + +- O: Yes +- X: No +- +: Tips + +| | constate | zustand | react-tracked | rematch | icestore | +| --------| -------- | -------- | -------- | -------- | -------- | +| Simplicity | ★★★★ | ★★★ | ★★★ | ★★★★ | ★★★★★ | +| Readability | ★★★ | ★★★ | ★★★ | ★★★ | ★★★★ | +| Configurable | ★★★ | ★★★ | ★★★ | ★★★★★ | ★★★ | +| Less boilerplate | ★★ | ★★★ | ★★★ | ★★★★ | ★★★★★ | +| Async Action | + | O | O | O | O | +| Class Component | + | + | + | O | O | +| Hooks Component | O | O | O | O | O | +| Async Status | X | X | X | O | O | +| Centralization | X | X | X | O | O | +| Model interaction | + | + | + | O | O | +| SSR | O | X | O | O | O | +| Lazy load models | + | + | + | O | O | +| Middleware or Plug-in | X | O | X | O | X | +| Devtools | X | O | X | O | X | + \ No newline at end of file diff --git a/examples/counter/.gitgnore b/examples/counter/.gitgnore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/examples/counter/.gitgnore @@ -0,0 +1 @@ +build diff --git a/examples/counter/package.json b/examples/counter/package.json new file mode 100644 index 00000000..80effecf --- /dev/null +++ b/examples/counter/package.json @@ -0,0 +1,34 @@ +{ + "name": "counter", + "version": "1.0.0", + "private": true, + "dependencies": { + "@ice/store": "^1.0.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" + }, + "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/counter/public/index.html b/examples/counter/public/index.html new file mode 100644 index 00000000..492089d1 --- /dev/null +++ b/examples/counter/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + Counter App + + + +
+ + + diff --git a/examples/counter/src/index.tsx b/examples/counter/src/index.tsx new file mode 100644 index 00000000..d97cf8d2 --- /dev/null +++ b/examples/counter/src/index.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { createStore } from '@ice/store'; + +const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time)); + +// 1️⃣ Use a model to define your store +const counter = { + state: 0, + actions: { + increment:(prevState) => prevState + 1, + async decrement(prevState) { + await delay(1000); + return prevState - 1; + }, + }, +}; + +const models = { + counter, +}; + +// 2️⃣ Create the store +const store = createStore(models); + +// 3️⃣ Consume model +const { useModel } = store; +function Counter() { + const [ count, actions ] = useModel('counter'); + const { increment, decrement } = actions; + return ( +
+ {count} + + +
+ ); +} + +// 4️⃣ Wrap your components with Provider +const { Provider } = store; +function App() { + return ( + + + + ); +} + +const rootElement = document.getElementById('root'); +ReactDOM.render(, rootElement); diff --git a/examples/counter/src/react-app-env.d.ts b/examples/counter/src/react-app-env.d.ts new file mode 100644 index 00000000..30da8962 --- /dev/null +++ b/examples/counter/src/react-app-env.d.ts @@ -0,0 +1 @@ +// / diff --git a/examples/counter/tsconfig.json b/examples/counter/tsconfig.json new file mode 100644 index 00000000..171592f7 --- /dev/null +++ b/examples/counter/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/todos/ice.config.js b/examples/todos/ice.config.js deleted file mode 100644 index f905f88e..00000000 --- a/examples/todos/ice.config.js +++ /dev/null @@ -1,30 +0,0 @@ -const path = require('path'); - -module.exports = { - entry: 'src/index.tsx', - publicPath: './', - alias: { - '@': path.resolve(__dirname, './src'), - }, - plugins: [ - ['ice-plugin-fusion', { - themePackage: '@icedesign/theme', - }], - ['ice-plugin-moment-locales', { - locales: ['zh-cn'], - }], - ], - chainWebpack: (config) => { - // 修改对应 css module的 loader,默认修改 scss-module 同理可以修改 css-module 和 less-module 规则 - ['scss-module'].forEach((rule) => { - if (config.module.rules.get(rule)) { - config.module.rule(rule).use('ts-css-module-loader') - .loader(require.resolve('css-modules-typescript-loader')) - .options({ modules: true, sass: true }); - // 指定应用loader的位置 - config.module.rule(rule).use('ts-css-module-loader').before('css-loader'); - } - }); - }, -}; - diff --git a/examples/todos/package.json b/examples/todos/package.json index d0cf0249..e536045d 100644 --- a/examples/todos/package.json +++ b/examples/todos/package.json @@ -4,18 +4,33 @@ "private": true, "dependencies": { "@ice/store": "^1.0.0", - "react": "16.8.6", - "react-dom": "16.8.6" + "lodash": "^4.17.15", + "react": "^16.8.6", + "react-dom": "^16.8.6" }, "devDependencies": { - "css-modules-typescript-loader": "^2.0.4", - "ice-plugin-fusion": "^0.1.4", - "ice-plugin-moment-locales": "^0.1.0", - "ice-scripts": "^2.0.0" + "@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": "ice-scripts dev", - "build": "ice-scripts build", - "test": "ice-scripts test" + "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/todos/public/index.html b/examples/todos/public/index.html index 5815fc96..552eacb1 100644 --- a/examples/todos/public/index.html +++ b/examples/todos/public/index.html @@ -5,11 +5,11 @@ - todos app + Todos App -
+
diff --git a/examples/todos/src/components/TodoList.tsx b/examples/todos/src/components/TodoList.tsx index 94da5664..8b2f9afd 100644 --- a/examples/todos/src/components/TodoList.tsx +++ b/examples/todos/src/components/TodoList.tsx @@ -3,7 +3,10 @@ import store from '../store'; const { useModel, useModelActionsState } = store; -export function TodoList({ title, subTitle, dataSource }, { toggle, remove }, actionsState) { +export function TodoList({ state, actions, actionsState }) { + const { title, subTitle, dataSource } = state; + const { toggle, remove } = actions; + return (

{title}

@@ -37,8 +40,10 @@ export default function({ title }) { const [ state, actions ] = useModel('todos'); const actionsState = useModelActionsState('todos'); return TodoList( - { ...state, title, subTitle: 'Function Component' }, - actions, - actionsState, + { + state: { ...state, title, subTitle: 'Function Component' }, + actions, + actionsState, + }, ); } diff --git a/examples/todos/src/components/TodoListClass.tsx b/examples/todos/src/components/TodoListClass.tsx index fb1d87f4..ff023d2a 100644 --- a/examples/todos/src/components/TodoListClass.tsx +++ b/examples/todos/src/components/TodoListClass.tsx @@ -1,10 +1,29 @@ import { Component } from 'react'; +import { Assign } from 'utility-types'; +import { UseModelValue, ModelActionsState } from '@ice/store'; +// import compose from 'lodash/fp/compose'; import store from '../store'; -import { TodoList } from './TodoList'; +import { TodoList as TodoListFn } from './TodoList'; +import todosModel from '../models/todos'; const { withModel, withModelActionsState } = store; -class TodoListClass extends Component { +interface MapModelToProp { + todos: UseModelValue; +} + +interface MapModelActionsStateToProp { + todosActionsState: ModelActionsState; +} + +interface CustomProp { + title: string; +} + +type PropsWithModel = Assign; +type Props = Assign; + +class TodoList extends Component { onRemove = (index) => { const [, actions] = this.props.todos; actions.remove(index); @@ -19,14 +38,17 @@ class TodoListClass extends Component { const { title, todos, todosActionsState } = this.props; const [ state ] = todos; const { dataSource } = state; - return TodoList( - { title, dataSource, subTitle: 'Class Component' }, - { toggle: this.onToggle, remove: this.onRemove }, - todosActionsState, - ); + return TodoListFn({ + state: { title, dataSource, subTitle: 'Class Component' }, + actions: { toggle: this.onToggle, remove: this.onRemove }, + actionsState: todosActionsState, + }); } } -export default withModelActionsState('todos')( - withModel('todos')(TodoListClass), +export default withModelActionsState('todos')( + withModel('todos')(TodoList), ); + +// functional flavor: +// export default compose(withModelActionsState('todos'), withModel('todos'))(TodoList); diff --git a/examples/todos/src/index.tsx b/examples/todos/src/index.tsx index 2558e2aa..8b74f171 100644 --- a/examples/todos/src/index.tsx +++ b/examples/todos/src/index.tsx @@ -27,5 +27,5 @@ function App() { ); } -const rootElement = document.getElementById('ice-container'); +const rootElement = document.getElementById('root'); ReactDOM.render(, rootElement); diff --git a/examples/todos/src/models/todos.ts b/examples/todos/src/models/todos.ts index 4a1a6972..1c2018a0 100644 --- a/examples/todos/src/models/todos.ts +++ b/examples/todos/src/models/todos.ts @@ -1,17 +1,26 @@ import { delay } from '../utils'; -interface Todo { +export interface Todo { name: string; done?: boolean; } -const store = { +export interface TodosState { + dataSource: Todo[]; +} + +const todos = { state: { - dataSource: [], + dataSource: [ + { + name: 'Init', + done: false, + }, + ], }, actions: { - async refresh(prevState, args, actions, globalActions) { + async refresh(prevState: TodosState, args, actions, globalActions) { await delay(2000); const dataSource: any[] = [ @@ -32,20 +41,20 @@ const store = { dataSource, }; }, - toggle(prevState, index: number) { + toggle(prevState: TodosState, index: number) { prevState.dataSource[index].done = !prevState.dataSource[index].done; return { ...prevState, }; }, - add(prevState, todo: Todo, actions, globalActions) { + add(prevState: TodosState, todo: Todo, actions, globalActions) { prevState.dataSource.push(todo); globalActions.user.setTodos(prevState.dataSource.length); return { ...prevState, }; }, - async remove(prevState, index: number, actions, globalActions) { + async remove(prevState: TodosState, index: number, actions, globalActions) { await delay(1000); prevState.dataSource.splice(index, 1); @@ -57,4 +66,4 @@ const store = { }, }; -export default store; +export default todos; diff --git a/examples/todos/src/models/user.ts b/examples/todos/src/models/user.ts index 0667c0d4..a5abe374 100644 --- a/examples/todos/src/models/user.ts +++ b/examples/todos/src/models/user.ts @@ -1,21 +1,9 @@ import { delay } from '../utils'; -export interface User { - name?: string; - age?: number; -} - -export interface UserStore { - dataSource: User; - auth: boolean; - todos: number; - login: () => void; - setTodos: (todos: number) => void; -} - -const store = { +const user = { state: { dataSource: { + name: '', }, todos: 0, auth: false, @@ -40,4 +28,4 @@ const store = { }, }; -export default store; +export default user; diff --git a/examples/todos/src/react-app-env.d.ts b/examples/todos/src/react-app-env.d.ts new file mode 100644 index 00000000..30da8962 --- /dev/null +++ b/examples/todos/src/react-app-env.d.ts @@ -0,0 +1 @@ +// / diff --git a/examples/todos/src/utils.ts b/examples/todos/src/utils.ts index 77adfb21..d94746a6 100644 --- a/examples/todos/src/utils.ts +++ b/examples/todos/src/utils.ts @@ -1 +1 @@ -export const delay = async (time) => new Promise((resolve) => setTimeout(() => resolve(), time)); +export const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time)); diff --git a/examples/todos/tsconfig.json b/examples/todos/tsconfig.json index 722f30fc..171592f7 100644 --- a/examples/todos/tsconfig.json +++ b/examples/todos/tsconfig.json @@ -1,32 +1,25 @@ { - "compileOnSave": false, - "buildOnSave": false, "compilerOptions": { - "baseUrl": ".", - "outDir": "build", - "module": "esnext", - "target": "es6", - "jsx": "react", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "lib": ["es6", "dom"], - "sourceMap": true, + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, - "rootDir": "src", - "forceConsistentCasingInFileNames": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noImplicitAny": false, - "importHelpers": true, - "strictNullChecks": true, - "suppressImplicitAnyIndexErrors": true, - "experimentalDecorators": true, - "noUnusedLocals": true, "skipLibCheck": true, - "paths": { - "@/*": ["./src/*"] - } + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" }, - "include": ["src/*"], - "exclude": ["node_modules", "build", "public"] + "include": [ + "src" + ] } diff --git a/package.json b/package.json index 454cf6ff..e361005b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ice/store", - "version": "1.0.0", + "version": "1.0.1", "description": "Lightweight React state management library based on react hooks", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/createModel.tsx b/src/createModel.tsx index cd8c26e9..fe791db8 100644 --- a/src/createModel.tsx +++ b/src/createModel.tsx @@ -6,37 +6,37 @@ import { createContainer } from './createContainer'; import { ReactSetState, ModelProps, - ModelConfig, + Config, ActionsPayload, SetActionsPayload, ActionsIdentifier, FunctionState, - TModel, - TModelConfigState, - TModelConfigActions, - TModelActions, - TModelActionsState, + Model, + ConfigPropTypeState, + ConfigPropTypeActions, + ModelActions, + ModelActionsState, SetFunctionsState, - TUseModelValue, + ModelValue, } from './types'; const isDev = process.env.NODE_ENV !== 'production'; -export function createModel(config: M, namespace?: K, modelsActions?): TModel { - type ModelState = TModelConfigState; - type ModelConfigActions = TModelConfigActions; - type ModelConfigActionsKey = keyof ModelConfigActions; - type ModelActions = TModelActions; - type ModelActionsState = TModelActionsState; - type SetModelFunctionsState = SetFunctionsState; - type UseModelValue = TUseModelValue; +export function createModel(config: C, namespace?: K, modelsActions?): Model { + type IModelState = ConfigPropTypeState; + type IModelConfigActions = ConfigPropTypeActions; + type IModelConfigActionsKey = keyof IModelConfigActions; + type IModelActions = ModelActions; + type IModelActionsState = ModelActionsState; + type SetModelFunctionsState = SetFunctionsState; + type IModelValue = ModelValue; const { state: defineState = {}, actions: defineActions = [] } = config; let actions; - function useFunctionsState(functions: ModelConfigActionsKey[]): - [ ModelActionsState, SetModelFunctionsState, (name: ModelConfigActionsKey, args: FunctionState) => void ] { - const functionsInitialState = useMemo( + function useFunctionsState(functions: IModelConfigActionsKey[]): + [ IModelActionsState, SetModelFunctionsState, (name: IModelConfigActionsKey, args: FunctionState) => void ] { + const functionsInitialState = useMemo( () => transform(functions, (result, name) => { result[name] = { isLoading: false, @@ -45,9 +45,9 @@ export function createModel(config: M, namesp }, {}), [functions], ); - const [ functionsState, setFunctionsState ] = useState(() => functionsInitialState); + const [ functionsState, setFunctionsState ] = useState(() => functionsInitialState); const setFunctionState = useCallback( - (name: ModelConfigActionsKey, args: FunctionState) => setFunctionsState(prevState => ({ + (name: IModelConfigActionsKey, args: FunctionState) => setFunctionsState(prevState => ({ ...prevState, [name]: { ...prevState[name], @@ -59,10 +59,10 @@ export function createModel(config: M, namesp return [ functionsState, setFunctionsState, setFunctionState ]; } - function useActions(state: ModelState, setState: ReactSetState): - [ ActionsPayload, (name: ModelConfigActionsKey, payload: any) => void, ModelActionsState ] { + function useActions(state: IModelState, setState: ReactSetState): + [ ActionsPayload, (name: IModelConfigActionsKey, payload: any) => void, IModelActionsState ] { const [ actionsState, , setActionsState ] = useFunctionsState(Object.keys(defineActions)); - const [ actionsInitialPayload, actionsInitialIdentifier ]: [ActionsPayload, ActionsIdentifier] = useMemo( + const [ actionsInitialPayload, actionsInitialIdentifier ]: [ActionsPayload, ActionsIdentifier] = useMemo( () => transform(defineActions, (result, action, name) => { const state = { payload: null, @@ -73,7 +73,7 @@ export function createModel(config: M, namesp }, [ {}, {} ]), [], ); - const [ actionsPayload, setActionsPayload ]: [ ActionsPayload, SetActionsPayload ] = useState(() => actionsInitialPayload); + const [ actionsPayload, setActionsPayload ]: [ ActionsPayload, SetActionsPayload ] = useState(() => actionsInitialPayload); const setActionPayload = useCallback( (name, payload) => setActionsPayload(prevState => ({ ...prevState, @@ -127,9 +127,9 @@ export function createModel(config: M, namesp return [ actionsPayload, setActionPayload, actionsState ]; } - function useModel({ initialState }: ModelProps): UseModelValue { - const preloadedState = initialState || (defineState as ModelState); - const [ state, setState ] = useState(preloadedState); + function useValue({ initialState }: ModelProps): IModelValue { + const preloadedState = initialState || (defineState as IModelState); + const [ state, setState ] = useState(preloadedState); const [ , executeAction, actionsState ] = useActions(state, setState); actions = useMemo(() => transform(defineActions, (result, fn, name) => { @@ -143,11 +143,11 @@ export function createModel(config: M, namesp } if (isDev && namespace) { - useModel.displayName = namespace; + useValue.displayName = namespace; } return createContainer( - useModel, + useValue, value => value[0], value => value[1], value => value[2], diff --git a/src/createStore.tsx b/src/createStore.tsx index b5e92683..b3d7ab89 100644 --- a/src/createStore.tsx +++ b/src/createStore.tsx @@ -2,15 +2,17 @@ import React from 'react'; import transform from 'lodash.transform'; import { createModel } from './createModel'; import { - ModelConfigs, - TModel, - TModelConfigState, - TModelActions, - TModelActionsState, + Optionalize, + Configs, + Model, + ConfigPropTypeState, + ModelActions, + ModelActionsState, + UseModelValue, } from './types'; -export function createStore(configs: C) { - function getModel(namespace: K): TModel { +export function createStore(configs: C) { + function getModel(namespace: K): Model { const model = models[namespace]; if (!model) { throw new Error(`Not found model by namespace: ${namespace}.`); @@ -28,58 +30,75 @@ export function createStore(configs: C) { return <>{children}; } - function useModelState(namespace: K): TModelConfigState { + function useModelState(namespace: K): ConfigPropTypeState { const [, useModelState ] = getModel(namespace); return useModelState(); } - function useModelActions(namespace: K): TModelActions { + function useModelActions(namespace: K): ModelActions { const [, , useModelActions ] = getModel(namespace); return useModelActions(); } - function useModelActionsState(namespace: K): TModelActionsState { + function useModelActionsState(namespace: K): ModelActionsState { const [, , , useModelActionsState ] = getModel(namespace); return useModelActionsState(); } - function useModel(namespace: K): [TModelConfigState, TModelActions] { + function useModel(namespace: K): UseModelValue { return [ useModelState(namespace), useModelActions(namespace) ]; } - function createWithUse(useFun) { - const fnName = useFun.name; - return function withModel(namespace, mapDataToProps?) { - const propName = fnName === useModel.name ? namespace : `${namespace}${fnName.slice(8)}`; - return (Component) => { - return (props): React.ReactElement => { - const model = useFun(namespace); - const modelProps = mapDataToProps ? mapDataToProps(model) : { [propName]: model }; - return ( - - ); - }; + function withModel) => Record>(namespace: K, mapModelToProps?: M) { + mapModelToProps = (mapModelToProps || ((model) => ({ [namespace]: model }))) as M; + return , P extends R>(Component: React.ComponentType

) => { + return (props: Optionalize): React.ReactElement => { + const value = useModel(namespace); + const withProps = mapModelToProps(value); + return ( + + ); }; }; } - function withModel(namespace: K, mapModelToProps?) { - return createWithUse(useModel)(namespace, mapModelToProps); - } - - function withModelActions(namespace: K, mapModelActionsToProps?) { - return createWithUse(useModelActions)(namespace, mapModelActionsToProps); + function withModelActions) => Record>(namespace: K, mapModelActionsToProps?: M) { + mapModelActionsToProps = (mapModelActionsToProps || ((actions) => ({ [`${namespace}Actions`]: actions }))) as M; + return , P extends R>(Component: React.ComponentType

) => { + return (props: Optionalize): React.ReactElement => { + const value = useModelActions(namespace); + const withProps = mapModelActionsToProps(value); + return ( + + ); + }; + }; } - function withModelActionsState(namespace?: K, mapModelActionsStateToProps?) { - return createWithUse(useModelActionsState)(namespace, mapModelActionsStateToProps); + function withModelActionsState) => Record>(namespace?: K, mapModelActionsStateToProps?: M) { + mapModelActionsStateToProps = (mapModelActionsStateToProps || ((actionsState) => ({ [`${namespace}ActionsState`]: actionsState }))) as M; + return , P extends R>(Component: React.ComponentType

) => { + return (props: Optionalize): React.ReactElement => { + const value = useModelActionsState(namespace); + const withProps = mapModelActionsStateToProps(value); + return ( + + ); + }; + }; } const modelsActions = {}; - const models: { [K in keyof C]?: TModel } = transform(configs, (result, config, namespace) => { + const models: { [K in keyof C]?: Model } = transform(configs, (result, config, namespace) => { result[namespace] = createModel(config, namespace, modelsActions); }); diff --git a/src/index.ts b/src/index.ts index a07cf409..422a4f04 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ +export * from './types'; export * from './createContainer'; export * from './createStore'; export * from './createModel'; -export * from './types'; diff --git a/src/types.ts b/src/types.ts index ff3caf6b..f097b631 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,7 @@ import * as React from 'react'; +export type Optionalize = Omit; + export type PropType = TObj[TProp]; export type ReactSetState = React.Dispatch>; @@ -57,19 +59,19 @@ export type ContextHookReturn< ? ContextHookTuple : ContextHookMultipleTuple); -export type ModelConfigAction = (prevState: S, payload?: any, actions?: any, globalActions?: any) => S | Promise; +export type ConfigAction = (prevState: S, payload?: any, actions?: any, globalActions?: any) => S | Promise; -export interface ModelConfigActions { - [name: string]: ModelConfigAction; +export interface ConfigActions { + [name: string]: ConfigAction; } -export interface ModelConfig { +export interface Config { state: S; - actions?: ModelConfigActions; + actions?: ConfigActions; }; -export interface ModelConfigs { - [namespace: string]: ModelConfig; +export interface Configs { + [namespace: string]: Config; } export interface ModelProps { @@ -82,7 +84,7 @@ export interface FunctionState { } export type FunctionsState = { - [K in keyof Functions]: FunctionState; + [K in keyof Functions]?: FunctionState; } export type SetFunctionsState = ReactSetState>; @@ -98,20 +100,29 @@ export interface ActionPayload { identifier: ActionIdentifier; } -export type ActionsPayload = { - [K in keyof ConfigActions]: ActionPayload; +export type ActionsPayload = { + [K in keyof A]: ActionPayload; } -export type SetActionsPayload = ReactSetState>; +export type SetActionsPayload = ReactSetState>; -export type Actions = { - [K in keyof ConfigActions]: (payload?: Parameters[1]) => void; +export type Actions = { + [K in keyof A]?: (payload?: Parameters[1]) => void; } -export type TModelConfigState = PropType; -export type TModelConfigActions = PropType; -export type TModelActions = Actions>; -export type TModelActionsState = FunctionsState>; -export type TUseModelValue = [ TModelConfigState, TModelActions, TModelActionsState ]; -export type TModel = - ContextHookReturn, TUseModelValue, [(model: TUseModelValue) => TModelConfigState, (model: TUseModelValue) => TModelActions, (model: TUseModelValue) => TModelActionsState]>; +export type ConfigPropTypeState = PropType; +export type ConfigPropTypeActions = PropType; +export type ModelActions = Actions>; +export type ModelActionsState = FunctionsState>; +export type ModelValue = [ ConfigPropTypeState, ModelActions, ModelActionsState ]; +export type Model = + ContextHookReturn< + ConfigPropTypeState, + ModelValue, + [ + (model: ModelValue) => ConfigPropTypeState, + (model: ModelValue) => ModelActions, + (model: ModelValue) => ModelActionsState + ] + >; +export type UseModelValue = [ ConfigPropTypeState, ModelActions ];