npm install react-use-promise-matcher
yarn add react-use-promise-matcher
This library provides two hooks that aim to facilitate working with asynchronous data in React. Implementing components that depend on some data fetched from an API can generate a significant amount of boilerplate code as it is a common practice to handle the following situations:
- The data hasn't been loaded yet, so we want to display some kind of loading feedback to the user.
- Request failed with an error and we should handle this situation somehow.
- Request was successful and we want to render the component.
Unfortunately, we cannot monitor the state of a Promise
as it is a stateless object. We can keep the information about the status of the request within the component's state though, however this usually forces us to repeat the same code across many components.
The usePromise
and usePromiseWithArguments
hooks let you manage the state of a Promise
without redundant boilerplate by using the PromiseResultShape
object, which is represented by four states:
Idle
- a request hasn't been sent yetLoading
- waiting for thePromise
to resolveRejected
- thePromise
was rejectedResolved
- thePromise
was resolved successfully
The PromiseResultShape
provides an API that lets you match each of the states and perform some actions accordingly or map them to some value, which is the main use case, as we would normally map the states to different components. Let's have a look at some examples then ...
Let's assume we have a simple echo method that returns the string provided as an argument wrapped in a Promise.
This is how we would use the usePromise
hook to render the received text based on what the method returns:
const echo = (text: string): Promise<string> => new Promise((resolve) => setTimeout(() => resolve(text), 3000));
export const EchoComponent = () => {
const [result, load] = usePromise<string>(() => echo("Echo!"));
React.useEffect(() => {
load();
}, []);
return result.match({
Idle: () => <></>,
Loading: () => <span>I say "echo!"</span>,
Rejected: (err) => <span>Oops, something went wrong! Error: {err}</span>,
Resolved: (echoResponse) => <span>Echo says "{echoResponse}"</span>,
});
};
The hook accepts a function that returns a Promise
, as simple as that. The type parameter defines the type of data wrapped by the Promise
. It returns an array with a result
object that represents the result of the asynchronous operation and lets us match its states and a load
function which simply calls the function provided as an argument within the hook.
It's also worth mentioning that matching the Idle
state is optional - the mapping for the Loading
state will be taken for the Idle
state if none is passed.
Sometimes it is necessary to pass some arguments to the promise loading function. You can simply pass such function as an argument to he usePromise
hook, it's type safe as the types of the arguments will be inferred in the returned loading function. It's also possible to explicitly define them in the second type argument of the hook.
export const UserEchoComponent = () => {
const [text, setText] = React.useState("Hello!");
const echoWithArguments = (param: string) => echo(param);
const [result, load] = usePromise<string, [string]>(echoWithArguments);
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => setText(e.target.value);
const callEcho = React.useCallback(() => {
() => load(text);
}, [load]);
return (
<div>
<input value={text} onChange={onInputChange} />
<button onClick={callEcho}>Call echo!</button>
{result.match({
Idle: () => <></>,
Loading: () => <span>I say "{text}"!</span>,
Rejected: (err) => <span>Oops, something went wrong! Error: {err}</span>,
Resolved: (echoResponse) => <span>Echo says "{echoResponse}"</span>,
})}
</div>
);
};
We can provide a third type parameter to the hook, which defines the type of error that is returned on rejection. By default, it is set to string. If we are using some type of domain exceptions in our services we could use the hook as following:
const [result] = usePromise<SomeData, [], MyDomainException>(() => myServiceMethod(someArgument));
and then we would match the Rejected
state like that:
result.match({
//...
Rejected: (err: MyDomainException) => err.someCustomField,
//...
});
If you need to repeatedly poll the data (eg. by sending a request to the server), and do that on-demand, you can use usePromiseWithInterval
hook. Pass the interval as a second argument, and receive the result
and start
& stop
functions in return. usePromiseInterval
uses setTimeout
API for polling.
export const UserEchoWithIntervalComponent = () => {
const echoWithArguments = (param: string) => echo(param);
const [result, start, stop] = usePromiseWithInterval<string, [string]>(echoWithArguments, 2000);
const startCallingEcho = React.useCallback(() => {
start("It's me again!!!");
}, [start]);
return (
<>
{result.match({
Idle: () => <></>,
Loading: () => <span>I say "{text}"!</span>,
Rejected: (err) => <span>Oops, something went wrong! Error: {err}</span>,
Resolved: (echoResponse) => <span>Echo says "{echoResponse}"</span>,
})}
</>
);
};
Besides that, you can also perform calls manually, and check the current amount of times your request was performed.
export const IntervalAndManualCheckComponent = () => {
const echoWithArguments = (param: string) => echo(param);
const [
result, // good old PromiseResultShape
start,
stop,
load, // manual load trigger
reset, // promise shape reset function
tryCount // amount of times your request was performed
] = usePromiseWithInterval<string, [string]>(echoWithArguments, 2000);
const startCallingEcho = React.useCallback(() => {
start("It's me again!!!");
}, [start]);
return (
<>
{result.match({
Idle: () => <></>,
Loading: () => <span>I say "{text}"!</span>,
Rejected: (err) => <span>Oops, something went wrong! Error: {err}</span>,
Resolved: (echoResponse) => <span>Echo says "{echoResponse}"</span>,
})}
</>
<button onClick={stop}>Stop</button>
<button disabled={retries < 3} onClick={load}>Retry manually</button>
);
};
Apart from rendering phase, you may want to perform some side effect functions when your promise is in a specific state. I.e. you may want to invoke another asynchronous function when the data is resolved or when the error is being thrown.
To do that, you can use callback functions dedicated to every promise state.
const [result1, load1] = usePromise<SomeData, [], MyDomainException>(() => myServiceMethod(someArgument));
const [result2, load2] = usePromise<SomeDataB, [Result1ResponseData], MyDomainException>(anotherServiceMethod);
result1
.onIdle(() => console.log("Promise is idle"))
.onLoading(() => console.log("Yaaay, bring the data on!"))
.onResolved((response) => load2(response.data))
.onRejected((err) => console.log(err.response.data));
React.useEffect(() => {
// run this after the component mounts
load1();
}, [load1]);
Every callback function is chainable - onIdle
, onLoading
, onResolved
and onRejected
return the PromiseResultShape
instance.