DEV Community

Simon Porter
Simon Porter

Posted on • Originally published at simonporter.co.uk

Slicing Data with Tanstack Query

Image description

I've been working with Tanstack Query for a while now and I'm a big, big, fan. I love how it works within my apps, nestled amongst my standard React code without needing a tonne of boilerplate, and how it takes care of the hard bits of caching for me.

Nobody wants to roll their own solutions to these hard problems1, and Tanstack Query "just works" for the projects I've used it on.

Big props to TkDodo for his continued support!

Slicing Data

Lately, I've been looking at slicing data with the select feature. TkDodo explains this on his blog: React Query Data Transformations but I thought I'd write up my learnings2 and talk about how to use them with TypeScript.

The select feature behaves like a redux selector. That is, it lets you take a larger piece of state, and slice it to extract just the parts you're interested in.

Quick Example

View the sample on Codesandbox!

The above example uses the useGetUser() custom hook which calls useQuery to request User data from our backend.

    export function useGetUser<T = User>({
        userId,
        options
    }: {
        userId: number;
        options?: {
            select?: (user: User) => T;
        };
    }) {
    return useQuery({
        queryKey: ["user", { userId }],
        queryFn: async () => {
            return dataOne as User;
        },
        ...options
    });
    }
Enter fullscreen mode Exit fullscreen mode

We've then created a custom useGetGender() hook which uses select to slice off just the gender value from this data.

    const selectGender = (user: User) => user?.gender;

    export function useGetGender({ userId }: { userId: number }) {
        return useGetUser({
            userId,
            options: {
                select: selectGender,
            },
        })
    }
Enter fullscreen mode Exit fullscreen mode

This is really powerful!

You can re-use the same state and slice off the bits you want without needing to make extra requests to other endpoints for the exact same data. Preventing more request waterfalls3 and potentially reducing rerenders4.

TypeScript Settings

You may have been expecting some Generic Gymnastics in the code above, in order to type the return values and support all options with UseQueryOptions<>.

For the most part, the recommendation is to let TypeScript do it's thing and infer the values for you. I know, cheating right?

    return useQuery({
        queryKey: ["user", { userId }],
        queryFn: async () => {
            return dataOne as User;
        },
        ...options
    });
Enter fullscreen mode Exit fullscreen mode

This is lying to TS to tell it we're returning a User, but unless you plan on adding Zod, this is about the best you can do.

This then lets TS infer the return of our own custom hook, because useQuery infers what data it will return from the queryFn and so our custom hook infers the same return type.

Image description

This makes sense, but what about the select ReturnType? That doesn't return the same data, and we could have multiple select's returning different data couldn't we?

We could always return what we need for this particular select, but that won't work for others with other data types.

    export function useGetUser({
        userId,
        options
    }: {
        userId: number;
        options?: {
            select?: (user: User) => string // Help??;
        };
    }) {
Enter fullscreen mode Exit fullscreen mode

Rather than start making unions for each data type you want returned, the solution is to use a generic on the custom hook to adjust the Return Type with a default.

This way if we use a select function in another custom hook, the return type of the select is passed through.

Otherwise, if no select is used, it defaults to the data we expect (the User).

Image description

Considerations

This isn't the only way to set these up though. You could go full hog and provide all the Generics, all the time, and type all the options (with UseQueryOptions<>) but... it's not recommended.

TkDodo has said that new Generics will be introduced in v5, and if you are currently only provide the existing ones, this may break on upgrade.

I'm happy with the trade offs from the above setup though, most inferrance with least duplication.


  1. "There are only two hard things in Computer Science: cache invalidation and naming things." - Phil Karlton 

  2. In my initial blog post, I write about why I wanted to write about my learnings. To cement my knowedge mostly, and if it's beneifical to others, that's a plus too! 

  3. Take a look at the GTMetrix blog for an explanation of request waterfalls. 

  4. It's actually a little more involved than that, while it can help, you should be aware of render optimizations in React Query first. 

Top comments (0)