I'm working with React and Apollo and I'm trying to initialize state with data fetched using useQuery hook but I'm getting "Cannot read property 'map' of undefined" when loading the page.
const { loading, error, data } = useQuery(FETCH_BLOGS);
const [state, setState] = useState(undefined);
useEffect(() => {
if (loading === false && data) {
setState(data.blogs);
}
}, [loading, data]);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
In the JSX I'm calling {renderBlogs(state)} which maps over the array and is where the error is being thrown. If I pass in the initial data {renderBlogs(data.blogs)} it works but I need to store the data in state as it will be mutated.
When I console.log(state) it logs 2 lines:
undefined.
It appears that the page is trying to render the initial state (undefined) before before the state is set to the query data. Is this the case? I thought using useEffect would solve this but that doesn't seem to be the case. Any help is appreciated, thank you.
To quote the useEffect documentation:
The function passed to
useEffectwill run after the render is committed to the screen. Think of effects as an escape hatch from React’s purely functional world into the imperative world.
The problem is that useEffect will trigger after a render. This will result in the following chain of events. Assume loading is just set to false and there where no errors fetching the data.
When the component is now being rendered both the if (loading) and if (error) guard clauses will be skipped, because the data successfully loaded. However state is not yet updated because the useEffect callback will trigger after the current render. At this point when you call renderBlogs(state) state will still be undefined. This will trow the error you describe.
I might be missing some context, but from what is shown in the question there is no reason to use useEffect. Instead use data directly.
The example provided by Apollo for useQuery provides a good starting point:
import { gql, useQuery } from '@apollo/client'; const GET_GREETING = gql` query GetGreeting($language: String!) { greeting(language: $language) { message } } `; function Hello() { const { loading, error, data } = useQuery(GET_GREETING, { variables: { language: 'english' }, }); if (loading) return <p>Loading ...</p>; return <h1>Hello {data.greeting.message}!</h1>; }
Applying the example to your scenario it might look very similar.
const { loading, error, data } = useQuery(FETCH_BLOGS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error</p>;
return renderBlogs(data.blogs);
// or
return <Blogs blogs={data.blogs} />;
// depending on what helps you visualize the solution better
If you need the setter you might want to provide a bit more context to the question. However a common reason to have a setter might be if you want to apply some sort of filter. A simple solution would be to have a computed blogs state.
const filteredBlogs = data.blogs.filter(blog => blog.title.includes(search));
Here search would be the state of some <input value={search} onChange={handleSearchChange} /> element. To improve performance you could also opt to use useMemo here.
const filteredBlogs = useMemo(() => (
data.blogs.filter(blog => blog.title.includes(search))
), [data, search]);
If you really need to use useState for some reason, you could try the onCompleted option of useQuery instead of useEffect.
const [blogs, setBlogs] = useState();
const { loading, error } = useQuery(FETCH_BLOGS, {
onCompleted: data => setBlogs(data.blogs),
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With