简体   繁体   中英

How do I set initial state to data fetched with useQuery?

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:

  • The first is undefined .
  • The second is the array of blogs (as shown in the screenshot).

屏幕截图控制台日志

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 useEffect will 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);

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(() => (
  blogs.filter(blog => blog.title.includes(search))
), [blogs, 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),
});

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM