![standard](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)
@airma/react-effect
@airma/react-effect
is designed for managing the asynchronous effect state for react components.
Why effects
React hook system is designed for synchronous render usage. But asynchronous operations are often used in components, so, take the asynchronous code out of render is a good choice. That's why useEffect
is useful in hook system, we can set the asynchronous in it, and run it out of the render time.
Good example:
import {useEffect, useState} from 'react';
import {query} from './service';
const useQueryEffect = (variables)=>{
const [data, setData] = useState(undefined);
const [isFetching, setFetching] = useState(false);
const [error, setError] = useState(undefined);
useEffect(()=>{
setFetching(true);
query(variables).then((d)=>{
setData(d);
setError(undefined);
setFetching(false);
},(e)=>{
setError(e);
setFetching(false);
});
},[variables]);
return {data, isFetching, error};
};
const App = memo(()=>{
......
const {data, isFetching, error} = useQueryEffect(variables);
return ......;
});
Not so good example:
import {memo, useState} from 'react';
import {query} from './service';
const App = memo(()=>{
const [data, setData] = useState(undefined);
const [isFetching, setFetching] = useState(false);
const [error, setError] = useState(undefined);
const handleQuery = async (variables)=>{
setFetching(true);
try {
const d = await query(variables);
setData(d);
setError(undefined);
} catch(e) {
setError(e);
} finally {
setFetching(false);
}
};
const handleReset = async ()=>{
await handleQuery(defaultVariables);
doSomething();
};
useEffect(()=>{
handleQuery();
},[]);
return ......;
})
Use asynchronous callback all over in component is not a good idea, we need concentrative controllers for limit asynchronous operations in less effects. Then we can have a simple synchronously render component.
Now, Let's take some simple, but more powerful API to replace the code of good example above.
useQuery
We can wrap a promise return callback to useQuery
, when the dependecy varaibles change, it fetches data for you.
import React from 'react';
import {useQuery} from '@airma/react-effect';
import {client} from '@airma/restful';
type UserQuery = {
name: string;
username: string;
}
const cli = client();
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [result, execute] = useQuery(
(q: UserQuery)=>
cli.rest('/api/user/list').
setParams(q).
get<User[]>(),
[query]
);
const {
data,
isFetching,
error,
isError
} = result;
......
}
If you want to execute the query manually, you can set manual:true
to the config.
import React from 'react';
import {useQuery} from '@airma/react-effect';
import {client} from '@airma/restful';
const cli = client();
const App = ()=>{
const [query, setQuery] = useState({name:'', username:''});
const [result, execute] = useQuery(
()=>
cli.rest('/api/user/list').
setParams(query).
get<User[]>(),
{manual: true}
);
const {
data,
isFetching,
error,
isError
} = result;
const handleClick = async ()=>{
const {
data,
isFetching,
error,
isError,
abandon
} = await execute();
}
......
}
The manual execution is not recommend, you may accept an abandoned result, if the execution is not the newest one, in that case, you may have a different result with the hook useQuery
result. And we have talked the problem about asynchronous code spread out in component.
useMutation
To execute a mutation, you can use useMutation
. It only can be executed manually.
import React from 'react';
import {useMutation} from '@airma/react-effect';
import {client} from '@airma/restful';
const cli = client();
const App = ()=>{
const [user, setUser] = useState({name:'', username:''});
const [result, execute] = useMutation(
(u:User)=>
cli.rest('/api/user').
setBody(u).
post<User>(),
[user]
);
const {
data,
isFetching,
error,
isError
} = result;
const handleClick = ()=>{
execute();
}
......
}
The different with useQuery
is that the useMutation
can not be truly executed again if the last execution is not finished, it returns the last result for you.
Sometimes we need an mutation only can be executed once. We can take a Strategy
like Strategy.once
.
import React from 'react';
import {useMutation, Strategy} from '@airma/react-effect';
import {client} from '@airma/restful';
const cli = client();
const App = ()=>{
const [user, setUser] = useState({name:'', username:''});
const [result, execute] = useMutation(
(u:User)=>
cli.rest('/api/user').
setBody(u).
post<User>(),
{
variables: [user],
strategy: Strategy.once()
}
);
const {
data,
isFetching,
error,
isError
} = result;
const handleClick = async ()=>{
const {
data,
isFetching,
error,
isError,
abandon
} = await execute();
}
......
}
state sharing
We have provides a EffectProvider
for sharing the state changes of useQuery
and useMutation
.
import React, {memo} from 'react';
import { client } from '@airma/restful';
import { useModel, useSelector, factory } from '@airma/react-state';
import { EffectProvider, asyncEffect, useAsyncEffect } from '@airma/react-effect';
type UserQuery = {name: string, username: string};
const cli = client();
const userQueryModel = (state: UserQuery)=>{
const {name, username} = state;
return {
name,
username,
state,
changeName(e: ChangeEvent){
return {username, name: e.target.value};
},
changeUsername(e: ChangeEvent){
return {name, username: e.target.value};
}
}
}
const queryUsers = (query:UserQuery)=> cli.rest('/api/user/list').
setParams(query).
get<User[]>();
const models = {
userQuery: factory(userQueryModel),
queryUsers: asyncEffect(queryUsers)
};
const Condition = memo(()=>{
const {
name,
username,
changeName,
changeUsername
} = useModel(models.userQuery);
const [{isFetching}, trigger] = useAsyncEffect(models.queryUsers);
return (
<div>
<input type="text" value={name} onChange={changeName}/>
<input
type="text"
value={username}
onChange={changeUsername}
/>
{/* we disable query button, */}
{/* when the query is fetching */}
<button
disabled={isFetching}
onClick={trigger}
>
query
</button>
</div>
)
});
const Datasource = memo(()=>{
const q = useSelector(models.userQuery,s =>s.state);
const [
{
data,
isFetching,
error,
isError
}
] = useQuery(models.queryUsers, [q]);
return ......;
});
const App = memo(()=>{
return (
<EffectProvider value={models}>
<Condition/>
<Datasource/>
</EffectProvider>
);
})
Now, you can share the query or mutation state any where in a EffectProvider
. Because the EffectProvider
is ModelProvider
, so, they have same features, for example, the useQuery or useAsynEffect find the key in parent Providers, the middle Provider will not block them. You can refer to ModelProvider in @airma/react-state.
async execution result
The promise result is a unitary result format for both useQuery and useMutation.
export declare type PromiseResult<T> = {
data: T | undefined;
error?: any;
isError: boolean;
isFetching: boolean;
abandon: boolean;
};
API
useQuery
To execute a query promise callback.
function useQuery<
D extends PromiseEffectCallback<any> | ModelPromiseEffectCallback<any>
>(
callback: D,
config?: QueryConfig<PCR<D>, MCC<D>> | Parameters<MCC<D>>
): [PromiseResult<PCR<D>>, () => Promise<PromiseResult<PCR<D>>>];
parameters:
- callback - a callback returns a promise, or a effect model. When it is a
effect model
, the query result will be shared out to any place in a EffectProvider. - config - it is optional. If you set nothing, it means you want to execute it manually. It can be an tuple array as parameters for callback. It can be a config object to set features of this query.
config:
- variables - you can set an array as parameters for query, when the elements change, the query callback runs.
- deps - you can set an array as dependencies, sometimes you may want to drive query callback running by the different dependencies with variables.
- manual - set manual
true
, means you want to execute the query manually, then the deps and variables change will not affect the query callback running. - strategy - you can set a strategy function or a strategy array to make query callback running with the strategy you want, for example:
debounce
, once
. If it is an array, the query follows running order from outside to inside. - primaryStrategy - the primaryStrategy provides final running strategies inside strategies. If you want to set strategies, please use
strategy
option, this option is often setted for replacing the global primaryStrategy from PrimaryStrategyProvider
.
returns:
[
result,
execute
]
useMutation
To execute a mutation promise callback, it can only be drived manually by calling the returning method execute
.
function useMutation<
D extends PromiseEffectCallback<any> | ModelPromiseEffectCallback<any>
>(
callback: D,
config?: MutationConfig<PCR<D>, MCC<D>> | Parameters<MCC<D>>
): [PromiseResult<PCR<D>>, () => Promise<PromiseResult<PCR<D>>>];
parameters:
- callback - a callback returns a promise, or a effect model. When it is a
effect model
, the query result will be shared out to any place in a EffectProvider. - config - it is optional. It can be an tuple array as parameters for callback. It can be a config object to set features of this mutation.
config:
- variables - you can set an array as parameters for query, when the elements change, the mutation callback runs.
- strategy - you can set a strategy function or a strategy array to make query callback running with the strategy you want, for example:
debounce
, once
. If it is an array, the query follows running order from outside to inside. - primaryStrategy - the primaryStrategy provides final running strategies inside strategies. If you want to set strategies, please use
strategy
option, this option is often setted for replacing the global primaryStrategy from PrimaryStrategyProvider
.
returns:
[
result,
execute
]
asyncEffect
It is used to generate a effect model
with effect( promise ) callback. We can provide it as a key to EffectProvider
or ModelProvider in @airma/react-state
for state sharing. And use useQuery
or useMutation
to link it, and fetching the query state.
function asyncEffect<
E extends (...params: any[]) => Promise<any>,
T = E extends (...params: any[]) => Promise<infer R> ? R : never
>(effectCallback: E): ModelPromiseEffectCallback<E>;
parameters:
- effectCallback - a callback returns a promise.
returns
A react-state factory model with effect( promise ) callback.
useAsyncEffect
It is used to accept the state change from useQuery
or useMutation
with a same effect model
.
function useAsyncEffect<
D extends ModelPromiseEffectCallback<any>
>(effectModel: D): [PromiseResult<PCR<D>>, () => void];
parameters:
- effectModel - an
effect model
created by asyncEffect
API.
returns:
[
result,
trigger
]
The trigger method is different with execute
method returned by useQuery
and useMutation
. It returns void, that means it can not be await
.
EffectProvider
You can refer it to ModelProvider in @airma/react-state
.
withEffectProvider
You can refer it to withModelProvider in @airma/react-state
.
PrimaryStrategyProvider
It is a react Provider
for setting global primaryStrategy for every useQuery
and useMutation
in children
.
import {
PrimaryStrategyProvider,
Strategy,
useQuery
} from '@airma/react-effect';
const App = ()=>{
useQuery(fetchUsers, [data]);
useQuery(fetchGroups, {
variables: [...ids],
strategy: [
Strategy.debounce(300),
Strategy.error(...)
],
primmaryStrategy: null
});
......
}
......
{}
<PrimaryStrategyProvider
value={Strategy.error(e => console.log(e))}
>
</PrimaryStrategyProvider>
Strategy
It provides some useful effect running Strategy
for you.
const Strategy:{
debounce: (op: { time: number } | number) => StrategyType,
once: () => StrategyType
error: (
process: (e: unknown) => any,
option?: { withAbandoned?: boolean }
) => StrategyType;
};
You can use it to the config strategy
in useQuery
and useMutation
.
For example:
import {
Strategy,
useQuery
} from '@airma/react-effect';
const useUserList = (...ids:number[])=>{
useQuery(fetchUsers, {
variables: [...ids],
strategy: [
Strategy.debounce(300),
Strategy.error((e) =>console.log(e))
]
})
};
debounce
you can set a debounce running time to it. like:
useQuery(callback,{
variables:[...],
strategy: Strategy.debounce({time:300})
})
Then the query callback runs with this debounce strategy.
once
It is used to force the query or mutation callback only runs once, if no error comes out.
error
You can set a callback to process the error information from promise rejection.
Use it as a global primary strategy can help you reduce the codes for dealing a common error process.
import {
Strategy,
PrimaryStrategyProvider
} from '@airma/react-effect';
const primary = Strategy.error((e) =>console.log(e));
<PrimaryStrategyProvider value={primary}>
......
</PrimaryStrategyProvider>
By the default, it only process the error result which is not abandoned. You can set {withAbandoned: true}
for dealing includes the abandoned errors.
import {
Strategy,
PrimaryStrategyProvider
} from '@airma/react-effect';
const primary = Strategy.error(
(e) =>console.log(e),
{withAbandoned: true}
);
<PrimaryStrategyProvider value={primary}>
......
</PrimaryStrategyProvider>
Write Strategy
You can write Strategy yourself, it is a simple work.
export type StrategyType<T = any> = (
getCurrentState: () => PromiseResult<T>,
runner: () => Promise<PromiseResult<T>>,
storeRef: { current: any }
) => Promise<PromiseResult<T>>;
A Strategy function accepts a parameter with properties:
- current - A callback returns a current promise result.
- runner - The wrapped effect callback, returns a promise.
- store - A store for your Strategy, you can store any thing which is helpful for your Strategy.
- variables - The variables for current query or mutation.
For example:
function once(): StrategyType {
return function oc(value: {
current: () => PromiseResult;
runner: () => Promise<PromiseResult>;
store: { current?: boolean };
}) {
const { current, runner, store } = value;
if (store.current) {
return new Promise(resolve => {
const currentState = current();
resolve({ ...currentState, abandon: true });
});
}
store.current = true;
return runner().then(d => {
if (d.isError) {
store.current = false;
}
return d;
});
};
};