Getting started
Install
sh
npm i normalized-react-query @tanstack/react-querysh
pnpm add normalized-react-query @tanstack/react-querysh
yarn add normalized-react-query @tanstack/react-queryTIP
@tanstack/react-query (v5) is a peer dependency, and does all the caching, refetching and subscribing. normalized-react-query is ESM only, and requires Node.js 24 or later for server code.
Define a resource
A resource pairs a query key with the function that loads it. Define it once, in a module of its own, and import it wherever the data is used:
ts
import { resource } from 'normalized-react-query';
export const users = resource({
key: ({ id }: { id: number }) => ['users', id],
queryFn: ({ params }) => getUser(params.id),
});keycomputes the key from the params. A resource that takes no params can use a fixed key:key: ['settings'].queryFnreceives the params, asparams, along with TanStack's usual context (signal,queryKey, …).- The params and data types come from these two functions. You can also give them explicitly:
resource<{ id: number }, User>({ ... }).
Use it in a component
Pass the resource and its params to a hook. The key and fetcher come from the resource, and the result is typed:
ts
import { skipToken } from '@tanstack/react-query';
import { useNormalizedQuery, useNormalizedSuspenseQuery } from 'normalized-react-query';
export const useUserName = (id: number | null) => {
// `skipToken` disables the query, as in TanStack
const { data } = useNormalizedQuery(users, id === null ? skipToken : { id }); return data?.name;
};
export const useUser = (id: number) => useNormalizedSuspenseQuery(users, { id }).data;Each hook takes the options of its TanStack counterpart (staleTime, select, enabled, …), except queryKey and queryFn, and optionally a QueryClient as its last argument. See the hooks reference.
Use the cache
Every cache operation takes the QueryClient, then the params, and is typed to the resource's data:
ts
import type { QueryClient } from '@tanstack/react-query';
export const rename = async (client: QueryClient, id: number, name: string) => {
users.setQueryData(client, { id }, user => user && { ...user, name });
await users.invalidateQuery(client, { id });
};
export const loadUser = async (client: QueryClient, id: number) => users.query(client, { id });Next steps
- Linked queries: start loading related data as soon as its parent loads.
- Infinite queries: paginated queries, defined the same way.
- Server rendering: load everything a page needs before rendering it.
- API reference: every method and hook.