@airma/react-effect
Version:
This is a react async state management tool
573 lines (478 loc) • 13.6 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-effect.svg?style=flat-square
[npm-url]: https://www.npmjs.com/package/%40airma/react-effect
[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-effect.svg?style=flat-square
# @airma/react-effect
`@airma/react-effect` is an asynchronous state-management tool for react.
## Document
* [English](https://filefoxper.github.io/airma/#/react-effect/index)
* [中文](https://filefoxper.github.io/airma/#/zh/react-effect/index)
## Code first
### useQuery
API `useQuery` can query data, and set it as a state.
```ts
import React from 'react';
import {useQuery} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
// Prepare a query promise callback.
const fetchUsers = (query: UserQuery):Promise<User[]> =>
Promise.resolve([]);
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [state, trigger, executeWithParams] = useQuery(
// Use query callback
fetchUsers,
// Set parameters for query callback
[query]
);
const {
// User[] | undefined
data,
// boolean
isFetching,
// any
error,
// boolean
isError,
// boolean
loaded
} = state;
......
}
```
When `useQuery` is mounted, or the dependency parameters change, it calls the promise callback.
### UseMutation
API `useMutation` is similar with `useQuery`. The difference is that it should be triggered manually to work.
```ts
import React from 'react';
import {useMutation} from '@airma/react-effect';
import {User} from './type';
const saveUser = (user: User): Promise<User> =>
Promise.resolve(user);
const App = ()=>{
const [user, setUser] = useState<User>({...});
const [
state,
trigger,
executeWithParams
] = useMutation(
// Set mutation callback,
// it is a promise callback.
saveUser,
// Set mutation parameters.
[ user ]
);
const {
// User | undefined
data,
// boolean
isFetching,
// any
error,
// boolean
isError
} = result;
const handleClick = ()=>{
// trigger mutation execute with variables [User]
trigger();
}
......
}
```
The state of useMutation has same fields with useQuery state.
### Session
Both of useQuery and useMutation need a promise callback for working, the mission of promise callback is called [session](/react-effect/concepts?id=session).
Use a simplified API [session](/react-effect/api?id=session) to make coding clear.
```ts
import React from 'react';
import {session} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
// use `session` API to declare a query session
const userQuerySession = session(
(query: UserQuery):Promise<User[]> =>
Promise.resolve([]),
'query'
);
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [
state,
trigger,
executeWithParams
// call session.useQuery
] = userQuerySession.useQuery(
// Set parameters for query callback
[query]
);
const {
// User[] | undefined
data,
// boolean
isFetching,
// any
error,
// boolean
isError,
// boolean
loaded
} = state;
......
}
```
The state of useQuery/useMutation is a local state. There are two different store state-managements: dynamic store or static store.
### Dynamic store state-management
```ts
import React from 'react';
import {session, provide} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
// declare a query session dynamic store
const userQueryKey = session(
(query: UserQuery):Promise<User[]> =>
Promise.resolve([]),
'query'
).createKey();
const SearchButton = ()=>{
// useSession subscribes state change from session store
const [
// state from session store
{isFetching},
// call trigger function to make useQuery work manually
triggerQuery
] = userQueryKey.useSession();
return (
<button
disabled={isFetching}
onClick={triggerQuery}
>
query
</button>
);
}
// provide keys to create a dynamic store in component
const App = provide(userQueryKey).to(()=>{
const [query, setQuery] = useState({name:'', username:''});
const [
state,
// Write every query state change to store
] = userQueryKey.useQuery(
[query]
);
......
return (
<>
<SearchButton />
......
</>
);
})
```
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.
### Static store state-management
```ts
import React from 'react';
import {session} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
// create a static store for session
const userQueryStore = session(
(query: UserQuery):Promise<User[]> =>
Promise.resolve([]),
'query'
).createStore();
const SearchButton = ()=>{
const [
{
isFetching,
// User[] | undefined
data
},
triggerQuery
] = userQueryStore.useSession();
return (
<button
disabled={isFetching}
onClick={triggerQuery}
>
query
</button>
);
}
// a static store needs no Provider.
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [
state
] = userQueryStore.useQuery(
[query]
);
......
return (
<>
<SearchButton />
......
</>
);
}
```
The state `data` from useSession is always has a `undefined` union type. API [useLoadedSession](/react-effect/api?id=useloadedsession) can be helpful if the session `state.data` is not empty from initializing time.
```ts
import React from 'react';
import {session} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
const userQueryStore = session(
(query: UserQuery):Promise<User[]> =>
Promise.resolve([]),
'query'
).createStore();
const SearchButton = ()=>{
// store.useLoadedSession can give out the promise resolve type without `empty`.
const [
{
isFetching,
// User[]
data
},
triggerQuery
] = userQueryStore.useLoadedSession();
return (
<button
disabled={isFetching}
onClick={triggerQuery}
>
query
</button>
);
}
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [
state
] = userQueryStore.useQuery(
// use object config to set default data
{
variables: [query],
// To make `state.data` not empty,
// a default data is needed.
defaultData: []
}
);
......
return (
<>
<SearchButton />
......
</>
);
}
```
Want to do something when query or mutation responses?
```ts
import React from 'react';
import {session, useResponse} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
const userQuerySession = session(
(query: UserQuery):Promise<User[]> =>
Promise.resolve([]),
'query'
);
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [
state
] = userQuerySession.useQuery(
[query]
);
// When useQuery/useMutation responses,
// useResponse calls the response callback.
useResponse(
// response callback
(sessionState)=>{
// accept a newest session state.
const {
data,
isError,
error,
......
} = sessionState;
doSomething(sessionState);
},
// listen to the session state of useQuery
state
);
// When useQuery/useMutation responses successfully,
// useResponse.useSuccess calls the response callback.
useResponse.useSuccess(
(data, sessionState)=>{
// accept a newst session state data.
// accept a newest session state.
doSomething(data);
},
// listen to the session state of useQuery
state
);
// When useQuery/useMutation responses unsuccessfully,
// useResponse.useFailure calls the response callback.
useResponse.useFailure(
(error, sessionState)=>{
// accept a newst session state error.
// accept a newest session state.
doSomething(error);
},
// listen to the session state of useQuery
state
);
......
}
```
Want to run useQuery or useMutation with some features like debounce?
### Strategy
```ts
import React from 'react';
import {session, Strategy} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
const userQuerySession = session(
(query: UserQuery):Promise<User[]> =>
Promise.resolve([]),
'query'
);
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [
state,
trigger,
executeWithParams
] = userQuerySession.useQuery(
{
variables: [query],
// set a debouce strategy to take debounce query feature.
strategy: Strategy.debounce(300)
}
);
......
}
```
The [Strategy](https://filefoxper.github.io/airma/#/react-effect/api?id=strategy) API contains some useful strategies for useQuery and useMutation. Compose some strategies together can make the session of useQuery/useMutation performance wonderfully.
```ts
import React from 'react';
import {session, Strategy} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
const userQuerySession = session(
(query: UserQuery):Promise<User[]> =>
Promise.resolve([]),
'query'
);
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [
state,
trigger,
executeWithParams
] = userQuerySession.useQuery(
{
variables: [query],
// compose different strategies.
strategy: [
// Validate query.name is not empty,
// if it is empty, then stop execute query
Strategy.validate(()=>!!query.name),
// Query with debounce feature
Strategy.debounce(300),
// If the response data equals current state.data,
// keeps current state.data.
Strategy.memo()
]
}
);
......
}
```
Want to use SWR(stale-while-revalidate)?
```ts
import React from 'react';
import {session, Strategy} from '@airma/react-effect';
import {User} from './type';
type UserQuery = {
name: string;
username: string;
}
const userQuerySession = session(
(query: UserQuery):Promise<User[]> =>
Promise.resolve([]),
'query'
);
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [
state,
trigger,
executeWithParams
] = userQuerySession.useQuery(
{
variables: [query],
strategy: [
// use swr strategy
Strategy.cache({
capacity:10,
staleTime:5*60*1000
})
]
}
);
......
}
```
## Introduce
`@airma/react-effect` is an asynchronous state-management tool for react. It dependents [@airma/react-state](https://filefoxper.github.io/airma/#/react-state/index), and there are some similar apis between both packages, so, use a common package [@airma/react-hooks](https://filefoxper.github.io/airma/#/react-hooks/index) is a better choice.
### Why not use setState in asynchronous callback?
Setting state in asynchronous callback is more easy to take a stale state usage bug in code. And it often makes [zombie-children](https://react-redux.js.org/api/hooks#stale-props-and-zombie-children) problem too.
### When useQuery works?
API useQuery works when it is mounted, or the dependency parameters change, just like React.useEffect performance. It also can be triggered manually.
## Install and Support
The package lives in [npm](https://www.npmjs.com/get-npm). To install the latest stable version, run the following command:
### Install command
```
npm i @airma/react-effect
```
### Browser support
```
chrome: '>=91',
edge: '>=91',
firefox: '=>90',
safari: '>=15'
```