I recently ran into a problem when doing a simple fetch request for a local database in react, This kind of problem is especially common among beginners just getting started with react and I was confident in my code when writing this routine fetch call as I have done so numerous of times without giving it a second thought.
Here is what I initially wrote to fetch the data from my simple local database that had 4 properties including the id:
useEffect(() => {
fetch(`http://localhost:4000/movies/${id}`)
.then((res) => res.json())
.then((data) => {
setMovie(data);
});
}, [id]);
And here is the full code that included updating state and mapping over the data that was coming from the local server:
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import NavBar from "../components/NavBar";
function Movie() {
const { id } = useParams();
const [movie, setMovie] = useState([]);
useEffect(() => {
fetch(`http://localhost:4000/movies/${id}`)
.then((res) => res.json())
.then((data) => {
setMovie(data);
});
}, [id]);
return (
<>
<main>
<NavBar />
<h1>{movie.title}</h1>
<p>{movie.time}</p>
{movie.genres.map((genre, index) => {
return <span key={index}>{genre} </span>;
})}
</main>
</>
);
}
export default Movie;
To my surprise, I saw uncaught TypeError: movie.genres is undefined
on my console.log on the browser and also got this Error page from my router.
I know I was not using some good practice standards like adding a catch method for the fetch but at the time this seemed like a simple logic that is bullet proof and could not fail.
Now my first instinct was to check the state and do a simple console.log console.log(movie)
I got an empty array, okay I see where the problem is, but why was I getting an empty array?
Maybe if I got rid of the variable in the dependencies array in the useEffect hook, it would magically solve my problems... that did nothing, the error was still there.
useEffect(() => {
fetch(`http://localhost:4000/movies/${id}`)
.then((res) => res.json())
.then((data) => {
setMovie(data);
});
}, []);
console.log(movie);
I understood that the state was not being impacted, whoever, won't fetching and updating the setter function cause the component to rerender and give me access to my precious data?
Debugging empty state and why rerender is not fixing the issue
I got deeper and tried to go with the execution of the program, understanding what was happening under the hood, at a superficial level of course since I could not start going into the babel complexities of transpiling, it would take me ages.
Let us start at the simple beginnings, I initialized the movie state with an empty array const [movie, setMovie] = useState([]);
so that when useEffect hook runs the fetch call, react renders the initial state value which is an empty array.
Our state update:
.then((data) => {
setMovie(data);
});
happens asynchronously so the component is not immediately rerendering with the fetched data during the fetch request.
useEffect(() => {
fetch(`http://localhost:4000/movies/${id}`)
.then((res) => res.json())
.then((data) => {
setMovie(data);
});
}, []);
React's render cycle
React will trigger a rerender whenever state is updated, in our case setMovie(data)
. However the rerender happens after the state has been set. During this initial render (before data is fetched and state updates), the code attempts to access movies.genres
assuming movie has the properties but movie is an empty array resulting in TypeError: Cannot read properties of undefined
How to fix this
Initialize state with null to more accurately reflect the state before data is fetched and makes it easier later to do some really interesting conditional render.
const [movie, setMovie] = useState(null);
after fetching the data:
useEffect(() => {
fetch(`http://localhost:4000/movies/${id}`)
.then((res) => res.json())
.then((data) => {
setMovie(data);
});
}, [id]);
We can add the conditional rendering:
if (!movie) {
return <p>Loading data...</p>;
}
This is particularly useful since we opted to use null instead of an empty array, since in javascript an empty array is truthy and this logic would not work out right, down the lines things will get much easier too...
Condtional render in JSX
In our jsx code we can do a conditional render like so
return (
<>
<main>
<NavBar />
<h1>{movie.title}</h1>
<p>{movie.time}</p>
{movie.genres &&
movie.genres.map((genre, index) => {
return <span key={index}>{genre} </span>;
})}
</main>
</>
);
What is happening here is that because we switched to using null, earlier when I was using empty arrays, it was rendering based on that, react did infact schedule a rerender, during the initial phase before the rerender happened, if the state was not meeting the expected value I was looking for, the entire logic fell apart.
The react could not rerender because of this unexpected error hence denying me access to the data.
The logic here:
{movie.genres &&
movie.genres.map((genre, index) => {
return <span key={index}>{genre} </span>;
})}
will work because during that initial phase, movies.genre
will be falsy hence there will be no errors derailing the flow of execution allowing react to rerender and updating the state
.then((data) => {
setMovie(data);
});
with the fetched data, hence removing that mysterious bug that can make a beginner new to this strange phenomenon spend hours debugging on stack overflow wondering why the code won't work!
Top comments (0)