@airma/react-state
Version:
the purpose of this project is make useReducer more simplify
276 lines (226 loc) • 9.39 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 `reducer-like` 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 `reducer-like` 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 `reducer-like` 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 `reducer-like` function has a simple name `model`. 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);
......
```
Though, the basic function about `model` is enhancing `React.useReducer` to manage a local state, it also can be used to manage a scope state from dynamic store or static store.
### Dynamic store state management
API `createKey` can create a model template for creating a dynamic store. The template is also a key to synchronize state changes from 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 synchronize state changes from store.
......
const Increase = memo(()=>{
// use countingKey.useSelector can synchronize 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 synchronize 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 synchronized in the children components by using `React.Context`.
A static store should be created in a global scope, and used 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
In `/react-state@18.4.0`, a more simple and higher performance API `useSignal` is provided.
```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>
);
};
```
The `useSignal` API is even better than API `useSelector`, it computes out when to rerender component by the fields getting from instance automatically. And by using the `signal` function, it always provides a newest instance in usage point, so it can avoid stale data and zombie-children problems more effectively.
## 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/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` and `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.
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'
```