A set of React custom hooks for data requesting and posting with xstate integrated, providing precise state transitions without the risk of yielding impossible states.
yarn add async-data-hooks
or
npm install --save async-data-hooks
import { useRequest } from 'async-data-hooks';
export const getDog = async (params?: { [x: string]: string }) => {
const res = await getJSON<DogResponseData>(`/api/puppies?${convertToString(params)}`));
return res.data;
};
const DogComponent = () => {
const { matcher, data, load } = useRequest<DogResponseData>({
name: 'cute puppies', // Optional
requestFn: getDog, // Function that takes in offset, limit and additional params, returns a promise
});
return (
<div>
<button onClick={() => load({ color: 'brown' })}>
Load brown dog
</button>
<button onClick={() => load()}>
Load default dog
</button>
{matcher.requesting && <p>Loading...</p>}
{matcher.success && <div>{JSON.stringify(data)}}
</div>
);
};
If your data is paginated, you need to handle the offset in your own component either in a state or url search param. The hook and state machine knows nothing about your current offset. Your requestFn
need to take into consideration the offset and request the correct page.
import { useRequest } from 'async-data-hooks';
const uploadCats = async (cat: Cat) => await postJSON('/api/cats', cat);
const CatsComponent = () => {
const { matcher, data, post } = useRequest<Cat, CatResponseData>({
name: 'upload cats', // Optional
requestFn: uploadCats, // Function that takes the data to post, returns a promise
});
return (
<div>
<button
onClick={() => {
post({
name: 'nyan cat',
color: 'pink grey',
description: 'omagad this cats pukes rainbows!!!',
});
}}
>
upload nyan cat
</button>
{matcher.requesting && <p>Uploading...</p>}
{matcher.success && (
<div>
<h1>Upload success, returned data:</h1>
{JSON.stringify(data)}
</div>
)}
</div>
);
};
Under the hood, a finite state machine determines what state data is in. You can access the state directly from xstateNode.value
, and compare it manually, or just use the matchState
function exposed, or use the very handy matcher
object that contains booleans like requesting
success
fail
for easy state matching.
With a state machine, all possible state transitions are pre-defined. The benefits of such approach is:
-
Impossible states are avoided automatically. For example:
- Problem can occur when your data is loaded but the
error
object andisError
flag are left unchanged from the last failed call. State machine only executes your side effect at the correct state and makes sure error is already cleared before it's executed, so such problem will not occur. - When you already fired a request but the user triggers another request before the previous one is resolved. You can try to avoid this by disabling the button, or adding a
isLoading
flag check before you execute the call, but this requires you to manage multiple flags as states, resetting them at the correct time and check them manually when needed. State machine elegantly solves this problem, it does not allow aREQUEST
event onFETCH_PENDING
state, so such request will not fire at all.
- Problem can occur when your data is loaded but the
-
Simplified way to check data state:
- you no longer need to manage multiple flags and checks like
!isLoading && !error && data !== null && <div>{data}</div>
. all you need ismatcher.success && <div>{data}</div>
.
- you no longer need to manage multiple flags and checks like
https://xstate.js.org/viz/?gist=e7160418a7b8ef1562659a710bdf1153
As the diagram illustrates:
- the machine starts from the
Idle
state, onREQUEST
event it will transition toRequestPending
state invokingrequestData
service at the same time. - if
requestData
resolves, it will senddone
event transitioning the machine toLoadFinished.LoadSucceeded
and executes the actionupdateData
to update the context - if
requestData
fails, it will senderror
event transitioning the machine toLoadFinished.LoadFailed
and executes the actionupdateError
to update the context LoadFinished
state (containing 2 sub states) allows anotherREQUEST
to be sent, also executesclearError
if it's inLoadFailed
state
- useRequest(config) ⇒
UseRequestReturnedObject
Create hook to perform a blocking request request for data (You can't request data when a request has not been resolved or rejected).
- UseRequestReturnedObject :
Object
useRequest(config) ⇒ UseRequestReturnedObject
Create hook to perform a blocking request request for data (You can't request data when a request has not been resolved or rejected).
Kind: global function
Returns: UseRequestReturnedObject
-
returnedObject
Param | Type | Description |
---|---|---|
config | useRequestHookConfig |
Config file with compulsory request function |
Kind: global typedef
Properties
Name | Type | Description |
---|---|---|
data | any |
xstateNode.context.data exposed for easy access to the data in the node |
error | any |
xstateNode.context.error exposed for easy access to the error in the node |
matcher | object |
object containing boolean values requesting, finished, success, fail for easy state maching |
load | function |
exposed function for loading data |
matchState | function |
exposed |
xstateNode | StateNode |
as the name implies, the representation of the current |