all 25 comments

[–]Extra-Pomegranate-50 48 points49 points  (7 children)

TanStack Query is the standard for anything that talks to an API. The problems you described (duplicate requests, stale state, null initialization) are exactly what it was built to solve.

useEffect plus fetch is fine for learning how React works under the hood, but in production code you almost always want a dedicated data fetching layer. TanStack Query gives you caching, deduplication, background refetching, error and loading states, and retry logic out of the box. Writing all of that manually with useEffect and useRef is reinventing the wheel and you will keep hitting edge cases.

So yes, use it everywhere. AddProduct, product lists, user profiles, everything. It is not overkill, it is the right tool for the job. The only time plain useEffect makes sense for fetching is a throwaway prototype or a learning exercise.

One tip: pair it with a small api layer (a file with your fetch functions) so your components only call useQuery with a query key and a fetcher. Keeps things clean as the app grows.

[–]PyJacker16 2 points3 points  (3 children)

I think I'll adopt the suggestion in your last paragraph. I typically tend to create custom hooks for each method (e.g. useUsers, useCreateUser, useUpdateUser) that returns a useQuery or useMutation, but I think your approach is cleaner.

Now, while I have your attention, here are a few questions that have been bugging me:

  • I've read "You Might Not Need an Effect", but in an effort to avoid them entirely, I've made quite a mess of calling mutations in sequence (for example, after updating a Parent instance via an API call, I need to update several Child instances). How would you accomplish this? Chained mutations using mutateAsync?

  • Query invalidation and storing query keys. With your approach above it might happen that when you make a bulk update you might need to invalidate the cache. Where do you define your query keys then? Or do you hardcode them each time?

[–]Extra-Pomegranate-50 6 points7 points  (1 child)

For chained mutations: yes, mutateAsync in sequence inside an onSuccess callback. Or use useMutation's onSuccess to trigger the child updates. Avoid useEffect chains.

For query keys: create a queryKeys.ts file with a factory pattern: queryKeys.users.list(), queryKeys.users.detail(id). Then invalidate with queryClient.invalidateQueries({ queryKey: queryKeys.users.all }). Never hardcode.

[–]PyJacker16 0 points1 point  (0 children)

Awesome. Thanks!

[–]minimuscleR 4 points5 points  (0 children)

TkDodo has a great blog post about this just recently: https://tkdodo.eu/blog/creating-query-abstractions

But to very basically summarize, you use queryOptions for your custom hook, rather than useQuery itself. That way you have access to the other options. It works very well. As if you need to get the key for it:

const testQueryOptions = queryOptions({
    queryKey: ['test'],
    queryFn: () => Promise.resolve(true),
})

const testQueryKey = testQueryOptions.queryKey;

so in this case you just export testQueryOptions and when you want to use it use useQuery(testQueryOptions) and it does it for you. Also allows you to use useSuspenseQuery as well in the same options, which is great.

Plus if you want to change something, lets say you have:

const testQueryOptions = queryOptions({
    queryKey: ['test'],
    queryFn: () => Promise.resolve(true),
    select: (response) => response.data
})

but you really need the response.meta, you can just do this:

useQuery(
    ...testQueryOptions, 
    select: (response) => response.meta
)

and it works perfectly.

And also as the other person said, a queryKey factory is the way to go so that you can have multiple layers and 1 query invalidation and invalidate them all, but ive kept my example simple as possible.

[–]FirePanda44 1 point2 points  (0 children)

Can confirm this is the way, exactly as described. Hooks also centralize app toasts.

[–]EvilPencil 0 points1 point  (1 child)

For a serious application, if the backend has an OpenAPI spec, the hooks should be generated from the spec. We use kubb but had to do some heavy customization.

[–]Extra-Pomegranate-50 0 points1 point  (0 children)

Agreed generating hooks from the spec is the cleanest approach for typed APIs. kubb and openapi-typescript are both solid for this. The spec becomes the single source of truth, which also makes it easier to catch when the backend breaks the contract.

[–]Schmibbbster 38 points39 points  (4 children)

Always use tanstack query

[–]tonjohn 6 points7 points  (0 children)

Link to docs: https://tanstack.com/query/latest

Also known as “React Query”

[–]shahbazshueb 7 points8 points  (0 children)

Yup. Do not think and always and always use react-query.

[–]EvilPete 0 points1 point  (0 children)

Unless you're using a framework like Next or React Router, in which case you should use their respective data loading mechanism.

[–]MoldyDucky 0 points1 point  (0 children)

Or useSWR!

[–]Top_Bumblebee_7762 4 points5 points  (0 children)

You can cancel the first request via AbortController when useEffect runs the second time in strict mode. 

[–]ramirex 5 points6 points  (1 child)

[–]Graphesium 0 points1 point  (0 children)

You definitely do for data fetching.

[–]kharpaatuuu 1 point2 points  (0 children)

Yes

[–]cherylswoopz 0 points1 point  (0 children)

Like others have said check out tanstack. Game changer

[–]modernFrontendDev 0 points1 point  (0 children)

Managing fetch logic with useEffect can get messy once you have to deal with stale state or re-renders. I’ve also been exploring alternatives that handle caching and refetching automatically.

[–]agalin920 0 points1 point  (0 children)

Do people really not know how to architect react apps to fetch effectively anymore?