UNPKG

@airma/react-state

Version:

the purpose of this project is make useReducer more simplify

353 lines (297 loc) 12.1 kB
[![npm][npm-image]][npm-url] [![NPM downloads][npm-downloads-image]][npm-url] [![standard][standard-image]][standard-url] [npm-image]: https://img.shields.io/npm/v/%40airma/react-state.svg?style=flat-square [npm-url]: https://www.npmjs.com/package/%40airma/react-state [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square [standard-url]: http://npm.im/standard [npm-downloads-image]: https://img.shields.io/npm/dm/%40airma/react-state.svg?style=flat-square # @airma/react-state Simple model state-management with method action dispatch mode for react components. ## Documents * [En](https://filefoxper.github.io/airma/#/react-state/index) * [中文](https://filefoxper.github.io/airma/#/zh/react-state/index) ## Code first Create model function: ```js export function counting(state:number){ return { // reproduced state for render count: state, // action method increase:()=>state + 1, // action method decrease:()=>state - 1, // action method, define parameters freely. add(...additions: number[]){ return additions.reduce((result, current)=>{ return result + current; }, state); } }; } ``` Use model function: ```tsx import {counting} from './model'; import {useModel} from '@airma/react-state'; ...... // give it an initialState can make it fly. const {count, increase, decrease, add} = useModel(counting, 0); // initialState `0` // call method `increase\decrease\add` can change `count` and make component rerender ...... ``` The model function returns an instance to manage the actions and state. Use API **model** can make it more simple. ### Local state management ```tsx import {model} from '@airma/react-state'; // api model returns a wrap function for your model function. // it keeps a same type of parameters and return data with the wrapped function. const counting = model(function counting(state:number){ return { count: state, increase:()=>state + 1, decrease:()=>state - 1, add(...additions: number[]){ return additions.reduce((result, current)=>{ return result + current; }, state); } }; }); ...... // you can get useModel from the model wrapped function. const {count, increase, decrease, add} = counting.useModel(0); ...... ``` This is just a local state management. It also can be used as a top store state management. ### Dynamic store state management API **createKey** can create a model wrapper for generating a dynamic store. It is also a key to subscribe this store. ```tsx import {memo} from 'react'; import {model, provide} from '@airma/react-state'; const countingKey = model(function counting(state:number){ return { count: state, increase:()=>state + 1, decrease:()=>state - 1, add(...additions: number[]){ return additions.reduce((result, current)=>{ return result + current; }, state); } }; }).createKey(0); // Create a key. // The key can be used to create a store. // The key can be used to subscribe state changes from store. ...... const Increase = memo(()=>{ // use countingKey.useSelector can subscribe state changes from store, // when the selected result is changed it rerender component. const increase = countingKey.useSelector(i => i.increase); return <button onClick={increase}>+</button>; }); const Count = memo(()=>{ // use countingKey.useModel can subscribe state changes from store. const {count} = countingKey.useModel(); return <span>{count}</span>; }); const Decrease = memo(()=>{ const decrease = countingKey.useSelector(i => i.decrease); return <button onClick={decrease}>-</button>; }); // A Hoc usage to create and provide a dynamic store to its children components. // It is same with using `Provider` Component to wrap the customized component. const Component = provide(countingKey).to(function Comp() { return ( <div> <Increase/> <Count/> <Decrease/> </div> ); }); ...... ``` A dynamic store should be created in a component, and be subscribed in the children components by using **React.Context**. A static store should be created in a global scope, and be subscribed in any component without provider. Using **model(xxx).createStore()** can build a static store. ### Static store state management ```ts import {model} from '@airma/react-state'; const countingStore = model(function counting(state:number){ return { count: state, increase:()=>state + 1, decrease:()=>state - 1, add(...additions: number[]){ return additions.reduce((result, current)=>{ return result + current; }, state); } }; }).createStore(0); // create a global store ...... const Increase = memo(()=>{ const increase = countingStore.useSelector(i => i.increase); return <button onClick={increase}>+</button>; }); const Count = memo(()=>{ const {count} = countingStore.useModel(); return <span>{count}</span>; }); const Decrease = memo(()=>{ const decrease = countingStore.useSelector(i => i.decrease); return <button onClick={decrease}>-</button>; }); // use global store without provider. const Component = function Comp() { return ( <div> <Increase/> <Count/> <Decrease/> </div> ); }; ``` The **useSelector** API is helpful for reducing render frequency, only when the selected result is changed, it make its owner component rerender. ### A high performance usage about useSignal API **useSignal** is designed to be a simple and high performance usage to replace **useSelector** in some cases. ```ts import {model} from '@airma/react-state'; const counting = model(function countingModel(state:number){ return { count: state, increase:()=>state + 1, decrease:()=>state - 1, add(...additions: number[]){ return additions.reduce((result, current)=>{ return result + current; }, state); } }; }).createStore(); // Give initialized state later in component render time. ...... const Increase = memo(()=>{ // API `useSignal` returns a signal function, // which can be called to get the newest instance from store. // Only the render usage fields of this instance change makes component rerender. // Here, only the action method `increase` from instance is required, and as the action method is stable with no change, that makes component never rerender. const signal = counting.useSignal(); return <button onClick={signal().increase}>+</button>; }); const Count = memo(()=>{ const signal = counting.useSignal(); return <span>{signal().count}</span>; }); const Decrease = memo(()=>{ const signal = counting.useSignal(); return <button onClick={signal().decrease}>-</button>; }); const Component = function Comp({defaultCount}:{defaultCount:number}) { // API `useSignal` can initialize store state in render too. // The difference with `useModel` is that `useSignal` only rerenders component when the render usage fields of instance changes. counting.useSignal(defaultCount); return ( <div> <Increase/> <Count/> <Decrease/> </div> ); }; ``` API **useSignal** is even better than API **useSelector**, it generates a signal callback. Call the signal callback can get a newest instance. The instance compares its render time using properties with the store instance properties, when these properties changes, it make **useSignal** rerenders component. ### Simulate asynchronous action ```ts import {memo} from 'react'; import {model} from '@airma/react-state'; const fetchSettingStep = ():Promise<number> =>{ return new Promise((resolve)=>{ setTimeout(()=>{ resolve(2); }, 300) }); } const countingKey = model(function counting(state:number){ return { count: state, increase:()=>state + 1, decrease:()=>state - 1, add(...additions: number[]){ return additions.reduce((result, current)=>{ return result + current; }, state); } }; // produce instance to be an asynchronous action simulate instance. }).produce((getInstance)=>{ // getInstance is a function to get current instance object. const instance = getInstance(); // returns a produced instance return { ...instance, // compose an async action async increaseBySetting(){ const step = await fetchSettingStep(); // use getInstance again to get the newest instance. return getInstance().add(step); }, async decreaseBySetting(){ const step = await fetchSettingStep(); return getInstance().add(-step); } } }).createKey(0); ...... const Increase = memo(()=>{ // The produced instance is changed by action works, and its methods are persist. const signal = countingKey.useSignal(); const {increaseBySetting} = signal(); return <button onClick={increaseBySetting}>+</button>; }); const Count = memo(()=>{ const {count} = countingKey.useModel(); return <span>{count}</span>; }); const Decrease = memo(()=>{ const signal = countingKey.useSignal(); const {decreaseBySetting} = signal(); return <button onClick={decreaseBySetting}>-</button>; }); // The HOC API `provide` can create store from keys in a `Provider` component. const Component = provide(countingKey).to(function Comp() { return ( <div> <Increase/> <Count/> <Decrease/> </div> ); }); ...... ``` The produced instance is not a true instance. It has persist methods, and these methods are normal methods, they can only change state by calling methods from getInstance(). ## Why support context store? The context store is a dynamic store, it has some better features than a static store. 1. The store data can be destroyed with its owner component unmount. 2. Components with same store factory creates different stores. ### How to subscribe a grand parent provider store? The store provider system in `@airma/react-state` is designed with a tree structure. The nearest **provider** finds store one-by-one from itself to its root parent **provider**, and links the nearest matched **provider** store to the subscriber **useModel/useSignal/useSelector**. ### Does the state change of store leads a whole provider component rerender? No, only the hooks subscribing this `store` may rerender their owners. Every store change is notified to its subscriber like **useModel** or **useSelector**, and then the subscriber rerenders its owner by **useState**. ## Why not support async action methods Async action often makes stale data problem and [zombie-children](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children) problem. So, a special tool to resolve this problem is necessary, you can try [@airma/react-effect](https://filefoxper.github.io/airma/#/react-effect/index) with it. And from **18.6.0**, you can use **model(xxx).produce** to compose action methods for simulating an asynchronous actions. There are more examples, concepts and APIs in the [documents](https://filefoxper.github.io/airma/#/react-state/index) of `@airma/react-state`. ## Browser Support ``` chrome: '>=91', edge: '>=91', firefox: '=>90', safari: '>=15' ```