First steps
In this section you will learn how to deal with the following using the library:
- Manually trigger data fetching
- Trigger data fetching based on dependencies
- Search while typing (+ concurrency & debounce)
- URL based automatic data fetching (via the query string)
The following examples are using the useAsync
hook.
Its signature is like this:
function useAsync(options, dependencies = []) {
// ...
}
Fetching the users list
Trigger the fetch on button's click
The following snippet should get you started to the library:
import axios from "axios";
import { useAsync, ProducerProps } from "react-async-states";
const API = axios.create({
baseURL: "https://jsonplaceholder.typicode.com",
});
type User = {
id: number;
email: string;
name: string;
};
async function fetchUsers({ signal }: ProducerProps<User[]>) {
// artificially delayed by 500ms
await new Promise((res) => setTimeout(res, 500));
return await API.get("/users", { signal }).then(({ data }) => data);
}
export default function App() {
const { data, isPending, source } = useAsync(fetchUsers);
return (
<div className="App">
<button disabled={isPending} onClick={() => source.run()}>
Fetch users {isPending && "..."}
</button>
{data && (
<ul>
<summary>Users list:</summary>
{data.map((user: User) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
);
}
Here is a codesandbox demo with the previous code snippet:
Fetching users list codesandbox demo
Make it automatic on mount
There are two ways to achieve this:
- use the
useAsync
hook with a configuration object while passing theproducer: fetchUsers
andlazy: false
properties. - use the
useAsync.auto(fetchUsers)
hook which adds thelazy: false
for you.
We used useAsync as follows in the previous example:
const result = useAsync(fetchUsers);
// this is the same as:
const result = useAsync({
producer: fetchUsers,
});
To make it automatic on component mount:
const result = useAsync({
lazy: false,
producer: fetchUsers,
});
// this is the same as:
const result = useAsync.auto(fetchUsers);
See it here using the useAsync.auto
variant:
Fetching users list automatically on mount codesandbox demo
Notice how nothing used by the useAsync
hook depend on the component render.
A simple:
useAsync.auto(producer);
Fetching the user details
Now, let's try to use variables from the component render phase.
First, let's make it ugly by storing a React.State
variable then pass it
to useAsync
. Then, let's eliminate the used state variable.
React to dependencies with condition
Now, let's fetch the user details when typing his id.
This time, we will be:
- Storing the
userId
in a state variable using ReactuseState
hook. - Pass the userId to our
producer
in theargs
for proper typing. - Only fetch if the userId is not empty and not
0
. - Fetch everytime the
userId
changes. - Abort the previous call if a second is done while
pending
.
Here is a full working example:
async function fetchUserDetails({
signal,
args: [userId],
}: ProducerProps<User, [string]>) {
// artificially delayed by 500ms
await new Promise((res) => setTimeout(res, 500));
return await API.get(`/users/${userId}`, { signal }).then(({ data }) => data);
}
export default function App() {
const [userId, setUserId] = useState("");
const { data, isPending, error } = useAsync.auto(
{
condition: !!userId,
autoRunArgs: [userId],
producer: fetchUserDetails,
},
[userId]
);
return (
<div className="App">
<input placeholder="userId" onChange={(e) => setUserId(e.target.value)} />
{isPending && "Loading..."}
{data && (
<details open>
<pre>{JSON.stringify(data, null, 4)}</pre>
</details>
)}
{error && (
<div>
error while retrieving user details
<pre>{error.toString()}</pre>
</div>
)}
</div>
);
}
Try it here, notice the cancellation of previous requests, also, you can remove the abort callback and/or the signal to make concurrency chaos, and make sure to observe the consistency in the UI.
react to dependencies change with condition codesandbox demo
Eliminate the previous state variable
Let's now use the run
function from the source
to fully eliminate any
component render variable or additional state:
async function fetchUserDetails({
signal,
args: [userId],
}: ProducerProps<User, [string]>) {
if (!userId) {
throw new Error("User Id is required");
}
return await API.get(`/users/${userId}`, { signal }).then(({ data }) => data);
}
export default function App() {
const { data, isPending, isSuccess, isError, error, source } =
useAsync(fetchUserDetails);
return (
<div className="App">
<input
placeholder="userId"
onChange={(e) => source.run(e.target.value)}
/>
{isPending && "Loading..."}
{isSuccess && (
<details open>
<pre>{JSON.stringify(data, null, 4)}</pre>
</details>
)}
{isError && (
<div>
error while retrieving user details
<pre>{error.toString()}</pre>
</div>
)}
</div>
);
}
Load user details as you type
Debounce search while typing
- We will slow down all requests by 500ms
- We will debounce by 400ms, so fetch will occur only after we hang on typing
- We will use the
state
property from theuseAsync
result to show extra information.
In useAsync
result:
state
refers to the current state.data
refers to the last success data.state.data
isdata
whenstate.status
issuccess
.
// ...
function App() {
const { state, source } = useAsync({
key: "user-details",
producer: fetchUserDetails,
// pass this args to the producer
// apply this effect to runs
runEffect: "debounce",
// this is the effect duration
runEffectDurationMs: 400,
});
return (
<div className="App">
<input
placeholder="userId"
onChange={(e) => source.run(e.target.value)}
/>
{state.status === "pending" &&
"Loading user with Id: " + state.props.args![0]}
{state.status === "success" && (
<details open>
<pre>{JSON.stringify(state.data, null, 4)}</pre>
</details>
)}
{state.status === "error" && (
<div>
error while retrieving user details
<pre>{state.data.toString()}</pre>
</div>
)}
</div>
);
}
Try it here:
debounce the run codesandbox demo
If you take a close look at how we used useState
in the previous example,
you'd see that our producer does not depend from any closure related to
the component render:
it can safely be moved to module level.
const searchUserConfig = {
producer: fetchUser,
runEffect: "debounce",
runEffectDurationMs: 400
};
export default function App() {
const { source, state } = useState(searchUserConfig);
// ... the rest
}
Skip the pending state if request is so fast
To skip the pending state, the skipPendingDelayMs
is used.
It means that when state turns to pending, and then changes under that delay, the pending update shall be skipped.
This makes your app feels synchronous.
const { state, source } = useAsync({
key: "user-details",
producer: fetchUserDetails,
// pass this args to the producer
// apply this effect to runs
runEffect: "debounce",
// this is the effect duration
runEffectDurationMs: 400,
// skip the pending status when the request is too fast
skipPendingDelayMs: 300,
});
See it in action here, and notice that when having a good internet connexion that the experience feels instantaneous.