<ForrigeUke uke="27" år="2025" />
En del av:
ForrigeUke<ForrigeUke /> er en artikkelserie som oppsummerer hva som skjedde i frontend-verden i uken som var.
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!
Del kunnskapen
Har du en kollega som også hadde dratt nytte av denne artikkelen?
Skrevet av
Mer fra Fag i Bekk
Nå er du ved veis ende. Gå til forsiden hvis du vil ha mer faglig påfyll.
Til forsiden