Zustand fetch with API call useEffect best practice
Asked Answered
A

2

22

When fetching state from an API with Zustand in a useEffect function what is the best practice for doing that? Right now I am using it very simply:

export interface ModeState{
  modes: Mode[];
  fetchModes: () => void;
}

export const useModeStore = create<ModeState>((set) => ({
  modes: [],
  fetchModes: async () => {
    const modes: AcquisitionMode[] = await API.get(`/acquisition-modes`);
    await set({ modes });
  },
}));

In component render function:

  const modeStore = useModeStore()
  const modes = modeStore.modes
  
  useEffect(() => {
    modeStore.fetchModes()
  }, [])

However the documentation seems to imply there are multiple ways this could be written to be more efficient in terms of performance, especially if my store grows more complex with more values and fetch functions. Is it best practice to make one store per API call? Use slices to get just the part of the store you need in each component? Should I be using the store differently in useEffect? I can't find a clear example online of how you should use the store in useEffect. The subscribe documentation does not seem to apply to the use case where you are using the store to fetch values with an async function.

Adalie answered 14/7, 2022 at 19:31 Comment(1)
I would personally use react-query to fetch the data and then set the result to the client state if only needed.Sarad
A
2

I have a suggestion: don't put your async method in the store, because:

  • It's not necessary with Zustand
  • Your store has more lines of code and is less readable

In the useEffect you lost dependency, it is better to use

useEffect(() => useModeStore.getState().fetchModes(), [])

And the important thing is the request status. You have two ways: write a lot of code every time or use third-party library for Zustand. I can offer this library (Zustand Fetching Helpers), it has everything you need to execute requests, does not overload the store and has a few more additional helpers. It is possible to repeatedly reduce boilerplate. Or you can copy the part you like.

For example with this library your code can looks like:

export interface ModeState{
  modes: Mode[];
}

export const useModeStore = create<ModeState>((set) => ({
  modes: [],
}));
export cosnt useModeRequest = leitenRequest(useModeStore, "modes", async () => {
    return API.get(`/acquisition-modes`);
  });

and component:

const modes = useModeStore(state => state.modes)
const status = useModeRequest(state => state.status)
  
  useEffect(() => {
    useModeRequest.action()
  }, [])

return status === "loaded" ? <>content</>

usually status and content within different components, but here is together

Artwork answered 19/5, 2023 at 14:18 Comment(0)
L
1

I have used zustand in a similar fashion in the past. I would often have a sync method on the store which I call in a useEffect and pass to it any state that is available to the component and not the store. Another possibility could be to let a library optimized for fetching get the data and make it available to the store once fetched.

What the documentation refers to with regard to performance is that you can indeed select parts of your store with a provided selector. In these cases a rerender will only happen when

  • the previous and current selected value are different or
  • a custom provided equality function states that previous and current values are different.

If you want to get into more detail with regard to performance I can recommend this this article here (disclaimer, I wrote it)

Even so, those performance considerations do not influence so much how you would trigger a fetch from, say, a useEffect hook.

Latini answered 18/2, 2023 at 23:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.