Often times, you want to render data that is fetched from a server. So, you make an API request to the server, set some state and render it out. But think about what is actually happening: your request is being called asynchronously. So, your render method will continue to be executed, even if the state from your server has not been set yet. Anytime you use fetch or axios, you will have to deal with this. Below an example scenario.
You may want to redirect a user if they are not authenticated or let them through if they are authenticated. You may now decide you want to program it like this:
const Component = () => {
const [state, setState] = useState({
auth: null,
});
const authenticate = () => {
fetch("/api/auth", {
method: "post",
})
.then((res) => res.json())
.then((_auth) => {
setState({
auth: _auth
})
});
};
useEffect(() => {
if(!state.auth) {
redirect();
} else {
doSomethingElse();
}
}, [state]);
return (
...
)
};
There are some problems with this approach, however. Right now, you will be immediately redirected. This is because the request we make has not yet updated the state before the useEffect call fires. So, what should happen if the authentication request is still in progress? Also, how can we differentiate between a failed call and a call that has not yet been made?
The solution is that we should wait for the authentication process to finish before redirecting the user if he is not authenticated. To do this, we need an additional parameter in the state to manage the status of the request.
const Component = () => {
const [state, setState] = useState({
auth: null,
status: "not-initialized",
});
const authenticate = () => {
fetch("/api/auth", {
method: "post",
})
.then((res) => res.json())
.then((_auth) => {
setState({
auth: _auth,
status: "auth-success",
});
})
.catch((e) => {
setState({
auth: null,
status: "auth-failed",
});
});
};
useEffect(() => {
if (state.status === "auth-failed") {
redirect();
} else if (state.status === "auth-success") {
doSomethingElse();
}
}, [state]);
return (
...
);
};
We now have an additional parameter status to which we can respond in our code. Now the code will work properly and not redirect us if the state is not yet ready.
But is this truly all we need? Maybe we need to know if the request is currently in progress. This can be done quite easily, with just a simple adaptation to our possible status states.
const { useEffect, useState } = require("react");
const Component = () => {
const [state, setState] = useState({
auth: null,
status: "not-initialized",
});
const authenticate = () => {
setState({
auth: null,
status: "in-progress",
});
fetch("/api/auth", {
method: "post",
})
.then((res) => res.json())
.then((_auth) => {
setState({
auth: _auth,
status: "auth-success",
});
})
.catch((e) => {
setState({
auth: null,
status: "auth-failed",
});
});
};
useEffect(() => {
if (state.status === "auth-failed") {
redirect();
} else if (state.status === "auth-success") {
doSomethingElse();
}
}, [state]);
return(
...
);
};
So that is all. Now you should be able to handle all possible states of your app. Maybe you want to add an error message to your state if the request failed. You can do it by changing the code on the catch(). You will know that you only have an error message to show if you are on the auth-failed state.
If you use Typescript, you can even add some nice type safety here:
type State =
| {
status: "not-initialized";
}
| {
status: "auth-success";
auth: {
name: string;
};
}
| {
status: "in-progress";
}
| {
status: "auth-failed";
message: string;
};
useState<State>({
status: "not-initialized",
});
As you can see, we removed the auth instead of setting it to null here, resulting in less code. Typescript prevents you from using 'auth' without checking that status equals 'auth-success'.