State & Data
This document covers client-side data fetching, cache management, global state, and internationalization patterns used in the application.
TanStack Query (Data Fetching)β
Why TanStack Queryβ
- Declarative data fetching and caching
- Background revalidation and stale-while-revalidate semantics
- Built-in support for pagination, infinite queries, and optimistic updates
Installationβ
pnpm add @tanstack/react-query
Provider setupβ
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export default function Providers({ children }: { children: React.ReactNode }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
Example usageβ
import { useQuery } from '@tanstack/react-query';
function useEmployees(page = 1) {
return useQuery(['employees', page], () =>
fetch(`/api/v1/employees?page=${page}`).then((r) => r.json()),
);
}
function EmployeesList() {
const { data, isLoading } = useEmployees(1);
if (isLoading) return <div>Loading...</div>;
return <EmployeesTable data={data.data} />;
}
Best practicesβ
- Use query keys that reflect parameters
- Use
selectto transform server responses - Keep cache times reasonable (
staleTime,cacheTime) - Use
prefetchQueryfor route transitions
Zustand (Lightweight Global State)β
Why Zustandβ
- Minimal API and small bundle size
- Works well for transient UI state and small global stores
- Simple to use with React and server components via client boundaries
Installationβ
pnpm add zustand
Example storeβ
import create from 'zustand';
type UIState = {
sidebarOpen: boolean;
toggleSidebar: () => void;
};
export const useUIStore = create<UIState>((set) => ({
sidebarOpen: false,
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));
Usageβ
'use client';
import { useUIStore } from '../stores/ui';
function SidebarToggle() {
const toggle = useUIStore((s) => s.toggleSidebar);
return <button onClick={toggle}>Toggle</button>;
}
Best practicesβ
- Use Zustand for UI state (modals, sidebars, feature flags)
- Keep domain state (entities) in TanStack Query cache or server
- Don't overuse global stores for deeply nested component state
Internationalization with next-intlβ
Why next-intlβ
- Designed to work with Next.js App Router
- Supports static & dynamic messages, locale routing, and server usage
Installationβ
pnpm add next-intl
Basic setupβ
- Create
i18nmessages undersrc/i18n/{locale}.json - Wrap root layout with
NextIntlProvideror use route-based loading
// app/layout.tsx (server component)
import { NextIntlProvider } from 'next-intl';
export default async function RootLayout({ children, params }) {
const messages = (await import(`../i18n/${params.locale}.json`)).default;
return (
<html lang={params.locale}>
<body>
<NextIntlProvider messages={messages}>{children}</NextIntlProvider>
</body>
</html>
);
}
Usage in componentsβ
'use client';
import { useTranslations } from 'next-intl';
export default function Greeting() {
const t = useTranslations('Home');
return <h1>{t('welcome')}</h1>;
}
Locale routingβ
- Use segment routes like
app/[locale]/page.tsxfor locale-aware routing - Provide a default locale and locale fallback strategy
Testing data & stateβ
- Use mocked responses for TanStack Query in unit tests
- Use isolated Zustand stores for component tests
Related docsβ
Resourcesβ
- TanStack Query: https://tanstack.com/query
- Zustand: https://zustand-demo.pmnd.rs/
- next-intl: https://next-intl-docs.vercel.app/