Reputation: 733
I have a component which fetch data from an API to display some details to user:
const ItemDetail = ({match}) => {
const [item, setItem] = useState(null);
useEffect(() => {
const abort = new AbortController();
fetchItem(abort);
return function cleanUp(){
abort.abort();
}
},[]);
const fetchItem = async (abort) => {
const data = await fetch(`https://fortnite-api.theapinetwork.com/item/get?id=${match.params.id}`, {
signal: abort.signal
});
const fetchedItem = await data.json();
setItem(fetchedItem.data.item);
}
return (
<h1 className="title">{item.name}</h1>
);
}
export default ItemDetail;
But when navigation reachs this component, the console shows the error Cannot access name of undefined, probably because the state was not updated yet.
Is it right to check item and return null if it was not updated yet? Something like this:
if(!item) return null;
return (
<h1 className="title">{item.name}</h1>
);
Or in that case should be better to use a class extended by React.Component and deal with its lifecycle properly?
Upvotes: 3
Views: 2926
Reputation: 1073978
You handle this in one of two ways:
Have the component render itself in a "loading" state, or
Don't create the component until you have the data — e.g., move the fetch operation into its parent, and only create the component once the parent has the data to render it (which you pass as props). (A specific example of the general principle of lifting state up.)
Because this comes up a fair bit, you might want to write a hook you can reuse, rather than having to re-write the logic every time. For instance, here's an example useFetchJSON
that uses fetch
to get and parse some JSON you might use in the child:
function useFetchJSON(url, init, deps) {
// Allow the user to leave off `init` even if they include `deps`
if (typeof deps == "undefined" && Array.isArray(init)) {
deps = init;
init = undefined;
}
if (!deps) {
console.warn(
"Using `useFetchJSON` with no dependencies array means you'll " +
"re-fetch on EVERY render. You probably want an empty dependency " +
"array instead."
);
}
const [loading, setLoading] = useState(true);
const [data, setData] = useState(undefined);
const [error, setError] = useState(undefined);
useEffect(() => {
setLoading(true);
setData(undefined);
setError(undefined);
fetch(url, init)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, deps);
return [loading, data, error];
}
Usage:
const [loading, data, error] = useFetchJSON(/*...*/, []);
Then use loading
(a flag set during loading), data
(the data that was loaded, if it isn't undefined
), or error (the error that occurred, if it isn't undefined
):
Live Example:
const { useState, useEffect } = React;
const fakeData = [
{
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": false
},
{
"userId": 1,
"id": 2,
"title": "quis ut nam facilis et officia qui",
"completed": false
},
{
"userId": 1,
"id": 3,
"title": "fugiat veniam minus",
"completed": false
},
{
"userId": 1,
"id": 4,
"title": "et porro tempora",
"completed": true
},
{
"userId": 1,
"id": 5,
"title": "laboriosam mollitia et enim quasi adipisci quia provident illum",
"completed": false
}
];
function fakeFetch(url, init) {
return new Promise((resolve) => {
// A fake delay to simulate network
setTimeout(() => {
resolve({
ok: true,
status: 200,
json() {
return Promise.resolve(fakeData);
}
});
}, 3000);
});
}
function useFetchJSON(url, init, deps) {
// Allow the user to leave off `init` even if they include `deps`
if (typeof deps == "undefined" && Array.isArray(init)) {
deps = init;
init = undefined;
}
if (!deps) {
console.warn(
"Using `useFetchJSON` with no dependencies array means you'll " +
"re-fetch on EVERY render. You probably want an empty dependency " +
"array instead."
);
}
const [loading, setLoading] = useState(true);
const [data, setData] = useState(undefined);
const [error, setError] = useState(undefined);
useEffect(() => {
setLoading(true);
setData(undefined);
setError(undefined);
fakeFetch(url, init)
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
return response.json();
})
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, deps);
return [loading, data, error];
}
// Usage:
function Example() {
const [loading, todos, error] = useFetchJSON("https://jsonplaceholder.typicode.com/todos/", []);
return (
<div>
{loading && <em>Loading todos...</em>}
{error && <strong>Error loading todos: {String(error)}</strong>}
{todos && (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)}
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Upvotes: 4