@airma/react-state
Version:
the purpose of this project is make useReducer more simplify
353 lines (297 loc) • 12.1 kB
Markdown
[![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
# /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 `/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'
```