<ForrigeUke uke="27" år="2025" />
Avanserte React Query-mønstre og livstegn fra Radix — i denne ukas ForrigeUke.
Dette var uka for 5-minutts-jobber og helt nye produkter — og 1907 ting som skjedde i frontendverdenen!
«<ForrigeUke /> er en artikkelserie som oppsummerer hva som skjedde i frontend-verden i uka som var.»

Avanserte React Query-mønstre
Youssef Benlemlih har lagd en video hvor han deler 15 (én av dem forsvant i redigeringen 🥲) mønstre i React Query. Selv om jeg har brukt React Query en del fra før, var det flere mønstre jeg ikke visste om. Også er det jo deilig å få mata noen eksempler, istedenfor å grave i docs. Her er noen av dem:
select for transformering
Den mest naturlige måten å transformere dataene, er å først hente data, så endre dem:
const { data } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
});
const fullNames = data?.map(user => `${user.firstName} ${user.lastName}`);
Men visste du at du kan transformere dataene i React Query før du tar dem i bruk? select lar deg gjøre det:
const { data: fullNames } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.map(user => `${user.firstName} ${user.lastName}`),
});
Dette har også en optimaliseringsgevinst, for med select
vil re-render bare skje om resultatet av select
endrer seg – ikke bare om det underliggende dataobjektet er nytt.
queryOptions for gjenbruk
Funksjoner jeg trenger flere steder pleier jeg å legge ett sted og eksportere for gjenbruk:
export function useUsersQuery(isLoggedIn: boolean) {
return useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
enabled: isLoggedIn,
});
}
Dette har problemet at om du skal sende inn flere tilvalg til query-en, får funksjonen stadig flere parametere, og som du i noen tilfeller ikke engang tar i bruk.
En måte å komme rundt dette på, er å ta i bruk queryOptions. Istedenfor å gjenbruke hele query-en, kan du heller gjenbruke det som alltid er likt:
// queries/users.ts
export const usersQueryOptions = queryOptions({
queryKey: ['users'],
queryFn: fetchUsers,
});
// someComp.tsx
export function UserList({ isLoggedIn }: { isLoggedIn: boolean }) {
const { data } = useQuery({
...usersQueryOptions,
enabled: isLoggedIn,
staleTime: 60000,
});
...
}
Mens tilvalg som om funksjonen er aktiv eller unntak på staleTime, kan du definere idet du bruker useQuery-en. Dette gir en utrolig fleksibel måte å bruke query-ene.
meta for tilleggsfunksjoner
Helt grunnleggende i React Query er dette mønsteret: 1) du henter data, og 2) når du oppdaterer den, invaliderer du dataene:
export function useUserQuery(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
}
export function useUpdateUserMutation() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateUser,
onSuccess: (_, updatedUser) => {
queryClient.invalidateQueries({
queryKey: ['user', updatedUser.id],
});
},
});
}
For hver mutering definerer vi useQueryClient, som vi bare bruker til å invalidere dataene i en onSuccess. Det kan bli en del duplikat. Hva om vi slapp å gjenta oss?
Med meta-nøkkelen i queryClient, kan vi definere ekstra felter på query-ene og muteringene våre. Ved å legge til et felt, for eksempel “invalidatesQuery”, kan vi med færre linjer kode få gjort dette vanlige mønsteret.
Da kan vi først sette opp funksjonen i queryClienten:
// setupQueryClient.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
mutationCache: {
onSuccess: (data, _variables, _context, mutation) => {
/* 👇 Ved suksess, hvis det fins en funksjon invalidatesQuery
på muteringen, invaliderer vi den gitte nøkkelen
*/
const invalidates = mutation.meta?.invalidatesQuery;
if (invalidates) {
queryClient.invalidateQueries({ queryKey: invalidates(data) });
}
},
},
});
Så når vi bruker useMutation, kan vi kalle på vår nye invalidatesQuery-funksjon:
export function useUpdateUserMutation() {
return useMutation({
mutationFn: updateUser,
meta: {
// 👇 Ved mutering oppgir vi nøkkelen via meta.invalidatesQuery-feltet
invalidatesQuery: (updatedUser: User) => ['user', updatedUser.id],
},
});
}
Er dette en forbedring?
For meg blir det litt for magisk, så jeg foretrekker den verbose varianten, så jeg vet hva som foregår. Men likevel artig å se bruk av queryClient-en for å sette opp gjenbrukbare funksjoner. Kanskje dette meta-feltet også er nyttig for annen funksjonalitet?
Paginering og prefetching
Benlemlih viser også hvordan du kan lage paginerings-funksjonalitet med useQuery. Det gjør du slik:
export function UsersTable() {
// 👇 Holde rede på nåværende side
const [page, setPage] = useState(1);
const { data } = useQuery(getUsersQueryOptions(page, 20));
function onNextPageClick() {
setPage(page + 1);
}
return (
<>
<UsersList users={data} />
<button onClick={onNextPageClick}>Neste side</button>
</>
);
}
Vi har en tilstand for å vite nåværende side, og bruker den verdien for å hente de relevante dataene.
En liten hack er å ikke bare hente for nåværende side, men også neste side. Da vil brukeren få se den neste hentingen umiddelbart:
export function UsersTable() {
const [page, setPage] = useState(1);
const queryClient = useQueryClient();
const { data } = useQuery(getUsersQueryOptions(page, 20));
// 👇 Hent data også for neste side
useEffect(() => {
queryClient.prefetchQuery(getUsersQueryOptions(page + 1, 20));
}, [page, queryClient]);
function onNextPageClick() {
setPage((prev) => prev + 1);
}
return (
<>
<UsersList users={data} />
<button onClick={onNextPageClick}>Neste side</button>
</>
);
}
Dette er ekstra nyttig hvis du vet at brukerne dine ofte klikker seg videre med én gang — da sparer du dem for ekstra spinnere.
Unngå spinner-helvete med suspense
Apropos spinnere, ser applikasjonen din slik ut?
https://x.com/acdlite/status/991503599246098432
Koden bak kan være en rekke datahentinger per komponent:
export function UsersTable() {
const {
data: users,
isLoading: isLoadingUsers,
} = useQuery(getUsersQueryOptions());
const {
data: roles,
isLoading: isLoadingRoles,
} = useQuery(getRolesQueryOptions());
const {
data: permissions,
isLoading: isLoadingPermissions,
} = useQuery(getPermissionsQueryOptions());
return (
<>
{isLoadingUsers && <p>Laster brukere...</p>}
{isLoadingRoles && <p>Laster roller...</p>}
{isLoadingPermissions && <p>Laster rettigheter...</p>}
{!isLoadingUsers && users && (
<UsersList users={users} roles={roles} permissions={permissions} />
)}
</>
);
}
Datahenting fra mange kilder kan føre til spinner-helvete og hoppende grensesnitt, såkalt “cumulative layout shift”. En rask løsning på det er å samle opp loading-tilstander:
export function UsersTable() {
...
const isLoading = isLoadingUsers || isLoadingRoles || isLoadingPermissions;
return (
<>
{isLoading && <p>Laster ...</p>}
{!isLoading && users && (
<UsersList users={users} roles={roles} permissions={permissions} />
)}
</>
);
}
Å samle laste-tilstander forutsetter at vi har all datahentingen på samme nivå, som kan føre til at vi mister kapsuleringen for hver komponent.
Et alternativ er å heller benytte suspense. Istedenfor å definere hvordan grensesnittet skal se ut når det laster inni komponenten, kan du heller hvile på nærmeste suspense-grense utenfor komponenten.
For å bruke suspense med React Query, bytter du ut useQuery med useSuspenseQuery, og så kan du fjerne fjerne loading-tilstandene:
export function UsersTable() {
const [page, setPage] = useState(1);
const { data: users } = useSuspenseQuery(getUsersQueryOptions(page, 20));
const { data: roles } = useSuspenseQuery(getRolesQueryOptions());
const { data: permissions } = useSuspenseQuery(getPermissionsQueryOptions());
return (
<>
<UsersList users={users} roles={roles} permissions={permissions} />
<button onClick={() => setPage((p) => p + 1)}>Neste side</button>
</>
);
}
Så sørge for at du har en suspense-grense utenfor:
<Suspense fallback={<Spinner />}>
<UsersTable />
</Suspense>
Suspense skjønner at data lastes, siden promise-et ikke er løst enda. Nå vises fallback-spinneren frem til dataene inni UsersTable er ferdig med å laste.
Raskere løsninger med optimistiske oppdatering
Benlemlih viser også to måter å gjøre et av de kuleste mønstrene jeg vet om: optimistisk oppdatering. Altså det å oppdatere grensesnittet før du vet at dataene er tilstede, som gir en mer responsiv brukeropplevelse.
Om videoen ikke gir deg nok forståelse for dette mønsteret, er det bare å ta en titt på en guide jeg har skrevet før, som også viser optimistisk oppdatering i React uten å bruke et bibliotek.
Enten du er ny eller gammel traver når det kommer til React Query, er Benlemlihs video absolutt verdt å ta en titt på:
Radix svarer på dramaet
Et par uker tilbake var Radix døende. Det virket hvert fall slik, basert på fuzzen rundt det. Jeg begynte å revurdere tidligere komponentbibliotek-valg, og for mitt kommende prosjekt, måtte jeg plutselig se etter alternativer.
Det var derfor gledelig å se at Radix forrige uke svarte på dramaet, og ønsker nå å streame ukentlig for å vise at de lever. Det ga nok flere designsystem-utviklere litt mer hvilepuls.
Likevel tror jeg konkurrent BaseUI fikk noen ekstra øyne på seg under dramaet, så jeg kommer til å følge med på om dette er et mer lovende alternativ. Uansett fint å se at et stødig komponentbibliotek som Radix fortsetter utviklingen. Første stream er allerede 10. juli.
👋 Det var alt for denne gang. Håper du har en fortsatt fin sommer, enten det er på kontoret eller en strand!