T
TanStack3w ago
correct-apricot

When to use a utility and when to use a custom hook with select?

I want to know what the best practice is for filtering data. I have a main hook that reaches an endpoint. And every time I need to process specific data, I create a new hook and use the main one, passing it a utility as a parameter. The problem is that the hooks are piling up... And I often end up using them in only one place in my application. Therefore, I'm not sure when I should create a new hook to handle data filtering and when I should use the base hook and pass it a utility to filter from the component
No description
13 Replies
other-emerald
other-emerald2w ago
I always create a generic hook
interface UseSomethingQueryProps<SelectedData = QueryFnReturnType> {
select: (data: QueryFnReturnType) => SelectedData
}

export default function useSomethingQuery<SelectedData = QueryFnReturnType>({ select }: UseSomethingQueryProps<SelectedData>){
return useQuery(somethingQueryOptions({ select }))
}
interface UseSomethingQueryProps<SelectedData = QueryFnReturnType> {
select: (data: QueryFnReturnType) => SelectedData
}

export default function useSomethingQuery<SelectedData = QueryFnReturnType>({ select }: UseSomethingQueryProps<SelectedData>){
return useQuery(somethingQueryOptions({ select }))
}
everytime there will be something different in other pages, i just create a new selector So in your case it would become
export const useAllSolicitudes = <TData>(select?: (solicitudes: Solicitude[]) => TData) => useQuery({
// immediately return
...
})

// where you use useTypesOfSolicitudes:
const typesOfSolicitudes = useAllSolicitudes(filterTypesOfSolicitudes)
export const useAllSolicitudes = <TData>(select?: (solicitudes: Solicitude[]) => TData) => useQuery({
// immediately return
...
})

// where you use useTypesOfSolicitudes:
const typesOfSolicitudes = useAllSolicitudes(filterTypesOfSolicitudes)
other-emerald
other-emerald2w ago
Render Optimizations | TanStack Query React Docs
React Query applies a couple of optimizations automatically to ensure that your components only re-render when they actually need to. This is done by the following means: structural sharing React Quer...
React Query Selectors, Supercharged
How to get the most out of select, sprinkled with some TypeScript tips.
other-emerald
other-emerald2w ago
On another note, it is recommended to not destructure spread the return of useQuery. If you need the isEmpty state, use select instead
correct-apricot
correct-apricotOP2w ago
In summary, if I need to use the generic hook that receives a select with a useful function many times, it is a good idea to create a custom hook. However, if I only need to use the useful function in one place, perhaps it does not make sense to create a hook? Could you give me an example of this? Thanks for the information, btw
other-emerald
other-emerald2w ago
Ooh oops i worded it wrong, not "destructuring" but "spread". It means so differently i am sorry. I will get back maybe tomorrow or several hours, currently afk
correct-apricot
correct-apricotOP2w ago
No problem! I'll be waiting
extended-salmon
extended-salmon2w ago
tanstack query optimizes how often it causes rerenders based on the properties you access from the query object. if you do ...query you technically access everything, pretty much disabling the "property tracking" system see this example: https://www.teemutaskula.com/blog/exploring-query-suspense#adding-types instead of doing { ...query, somethingElse: ...} it mutates the already existing query object without accessing anything else that's not strictly required
correct-apricot
correct-apricotOP2w ago
That's good to know, and thank you for sending it to me, but right now I prefer to have understandable code, and I think what you sent me adds quite a bit of complexity. In the future, if I want to optimize it, I will definitely apply it What is your opinion on this? @ferretwithabéret
extended-salmon
extended-salmon2w ago
I usually use functions to create the query options I.e.
const getTodoListQueryOptions = () => queryOptions({
queryKey: ['todo', 'list'],
queryFn: /* ... */
})

const query = useQuery(getTodoListQueryOptions())

// Or

const query = useQuery({
...getTodoListQueryOptions(),
select: useCallback((data) => /* ... */)
})
const getTodoListQueryOptions = () => queryOptions({
queryKey: ['todo', 'list'],
queryFn: /* ... */
})

const query = useQuery(getTodoListQueryOptions())

// Or

const query = useQuery({
...getTodoListQueryOptions(),
select: useCallback((data) => /* ... */)
})
then, if I need to reuse a query in multiple places I move it somewhere else, like services/api/queries.ts and I define the common options there.
export useTodosWithSelect = () => {
return useQuery({
...getTodoListQueryOptions(),
select: useCallback((data) => /* ... */)
})
}
export useTodosWithSelect = () => {
return useQuery({
...getTodoListQueryOptions(),
select: useCallback((data) => /* ... */)
})
}
Snippet from the project I am actively working on RN:
import { MeData } from "@/utils/types";
import { getMeQueryOptions, useRedactedApiConfig } from "[REDACTED]";
import { useCallback } from "react";
import { useCustomQuery } from "../query/hooks";

export const useMe = () => {
const config = useRedactedApiConfig();
return useCustomQuery<MeData>({
...getMeQueryOptions({ config }),
freshWhileOnline: false,
refetchOnScreenFocus: false,
select: useCallback(
(data: MeData) => ({
...data,
local_data: {
...data.local_data,
roles: data.local_data.roles.filter((role) =>
["teacher", "parent", "student"].includes(role.type)
),
},
}),
[]
),
});
};
import { MeData } from "@/utils/types";
import { getMeQueryOptions, useRedactedApiConfig } from "[REDACTED]";
import { useCallback } from "react";
import { useCustomQuery } from "../query/hooks";

export const useMe = () => {
const config = useRedactedApiConfig();
return useCustomQuery<MeData>({
...getMeQueryOptions({ config }),
freshWhileOnline: false,
refetchOnScreenFocus: false,
select: useCallback(
(data: MeData) => ({
...data,
local_data: {
...data.local_data,
roles: data.local_data.roles.filter((role) =>
["teacher", "parent", "student"].includes(role.type)
),
},
}),
[]
),
});
};
correct-apricot
correct-apricotOP2w ago
Thanks for the snippet
other-emerald
other-emerald2w ago
export interface ListEntityQueryProps<Selected = ListEntityResponseDto> {
// add more props when more customization want to be added
selector?: (data: ListEntityResponseDto) => Selected
search?: string
}

export default function listEntityQuery<Selected = ListEntityResponseDto>({
selector = identity,
search,
}: ListEntityQueryProps<Selected>) {
return queryOptions({
queryKey: ['list-entity', search],
queryFn: ... REDACTED...,
select: selector,
})
}

interface ListEntitySuspenseQueryProps<Selected = ListEntityResponseDto> {
selector?: (data: ListEntityResponseDto) => Selected
search?: string
}

export default function useListEntitySuspense<Selected = ListEntityResponseDto>({
selector = identity,
search,
}: ListEntitySuspenseQueryProps<Selected>) {
return useSuspenseQuery(listEntityQuery<Selected>({ selector, search }))
}
export interface ListEntityQueryProps<Selected = ListEntityResponseDto> {
// add more props when more customization want to be added
selector?: (data: ListEntityResponseDto) => Selected
search?: string
}

export default function listEntityQuery<Selected = ListEntityResponseDto>({
selector = identity,
search,
}: ListEntityQueryProps<Selected>) {
return queryOptions({
queryKey: ['list-entity', search],
queryFn: ... REDACTED...,
select: selector,
})
}

interface ListEntitySuspenseQueryProps<Selected = ListEntityResponseDto> {
selector?: (data: ListEntityResponseDto) => Selected
search?: string
}

export default function useListEntitySuspense<Selected = ListEntityResponseDto>({
selector = identity,
search,
}: ListEntitySuspenseQueryProps<Selected>) {
return useSuspenseQuery(listEntityQuery<Selected>({ selector, search }))
}
function entitySelector(data: ListEntityResponseDto) {
return {
isEmpty: data.length === 0,
array: data.data,
}
}

function Component() {
const { data } = useListEntitySuspense({
selector: entitySelector,
})

// data.isEmpty and data.array are accessible, for non-suspense, data?.isEmpty, data?.array
}
function entitySelector(data: ListEntityResponseDto) {
return {
isEmpty: data.length === 0,
array: data.data,
}
}

function Component() {
const { data } = useListEntitySuspense({
selector: entitySelector,
})

// data.isEmpty and data.array are accessible, for non-suspense, data?.isEmpty, data?.array
}
That is how I normally do it, returning as it is without spreading. and access only the needed properties when using the hook
conscious-sapphire
conscious-sapphire2w ago
this is the best approach!
correct-apricot
correct-apricotOP2w ago
What is the balance between performance and reuse with selectors? I understand that, ideally, hooks should always return what you need with selectors, but if at some point I no longer need a field and it is a reused hook in my application, should I create a specific selector?

Did you find this page helpful?