Search code examples
reactjstypescriptreact-hooksreact-queryzustand

How to use zustand to store the result of a query


I want to put the authenticated user in a zustand store. I get the authenticated user using react-query and that causes some problems. I'm not sure why I'm doing this. I want everything related to authentication can be accessed in a hook, so I thought zustand was a good choice.

This is the hook that fetches auth user:

const getAuthUser = async () => {
  const { data } = await axios.get<AuthUserResponse>(`/auth/me`, {
    withCredentials: true,
  });
  return data.user;
};

export const useAuthUserQuery = () => {
  return useQuery("auth-user", getAuthUser);
};

And I want to put auth user in this store:

export const useAuthStore = create(() => ({
  authUser: useAuthUserQuery(),
}));

This is the error that I get:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons.

you can read about it in the react documentation: https://reactjs.org/warnings/invalid-hook-call-warning.html

enter image description here (I changed the name of some functions in this post for the sake of understandability. useMeQuery = useAuthUserQuery)

I understand the error but I don't know how to fix it.


Solution

  • The misunderstanding here is that you don’t need to put data from react query into any other state management solution. React query is in itself a global state manager. You can just do:

    const { data } = useAuthUserQuery()

    in every component that needs the data. React query will automatically try to keep your data updated with background refetches. If you don’t need that for your resource, consider setting a staleTime.

    —-

    That being said, if you really want to put data from react-query into zustand, create a setter in zustand and call it in the onSuccess callback of the query:

    useQuery(key, queryFn, { onSuccess: data => setToZustand(data) })
    

    edit April 2023:

    The advice about the onSuccess callback is not a good one. It seems that I didn't fully understand the consequences at the time of posting this answer.

    We have deprecated the callbacks on useQuery and will remove them in the next major version (v5) The callbacks won't reliably run, and if they do, they will run for every instance, which is not what you want if you want to write data to zustand. The better way would be to setup a useEffect:

    const { data } = useQuery({ queryKey, queryFn })
    
    React.useEffect(() => {
      if (data) {
        setToZustand(data)
      }
    }, [data])
    

    but this still has a bunch of drawbacks, like adding an extra render cycle, and running twice in React18 StrictMode (in dev env). So the initial advice - you don't need to sync data anywhere - still stands.

    I wrote a detailed blogpost about this.