React guidelines
We bootstrap React applications with Next.js (App Router) by default. Use Vite for single-page apps that don't need server-side rendering, SEO or server code. Create React App, which we used on older projects, was deprecated by the React team in 2025 and must not be used for new projects. For mobile apps, see our React Native guidelines, which build on this guide.
1.1. Use TypeScript
1.7. Do name exports
1.8. Do name prop types
3.1. State management
3.1.1. Local state management
3.1.2. Global state management
3.2. External services
3.3. GraphQL
3.4. REST
3.5. Routing
3.5.1. Next.js App Router
3.5.2. Route definitions
3.5.3. Access route parameters
3.7. Testing
1. Do's and Don'ts
1.1. Use TypeScript
Always. Start from the TypeScript template of your framework and follow our TypeScript guidelines. The rules below (1.6 to 1.8) complement that guide with React-specific conventions.
1.2. Use a generator to bootstrap the project
A project generator saves a lot of boilerplate work and provides common conventions.
Default: Next.js with the App Router:
npx create-next-app@latest(pick TypeScript, ESLint, App Router and thesrc/directory).Single-page apps without SSR, SEO or server code: Vite:
npm create vite@latest -- --template react-ts.
Older projects were bootstrapped with Create React App, but it's deprecated and must not be used for new projects.
1.3. Write function components
Functions and hooks are the standard. Class components are legacy: never write new ones, and migrate them when you touch them.
1.4. Use hooks for state management
In general, prefer React's hooks for local state management
For global state, prefer server-side state: keep the server as the source of truth and read it with Server Components or TanStack Query
Keep shareable UI state (pagination, filters, search, sort, active tab) in the URL search params, so a link reproduces the exact view
If server-side state is not possible, prefer React Context for simple state (theme, current user, feature flags)
For more complex client-side state, use a library (with hooks), for example zustand
Avoid redux. If a project already depends on it or it's unavoidable, use Redux Toolkit and its hooks API
See patterns (below) for examples and usages.
1.5. Use a declarative API library
It reduces boilerplate code a lot.
We currently use:
REST: TanStack Query (formerly react-query)
GraphQL: apollo-client
Don't write API types by hand. For external REST APIs, generate the types and the client from their OpenAPI spec with Hey API (@hey-api/openapi-ts). See REST.
In Next.js, Server Components fetch data directly and don't need a client library. TanStack Query is for client components. See Server and client components.
1.6. Do use function declarations
For a better readability.
1.7. Do name exports
It allows to export multiple values and it encourages the use of the same naming.
The exception is Next.js file conventions: page.tsx, layout.tsx, loading.tsx, error.tsx and not-found.tsx require a default export, and route handlers export named GET, POST, etc. Use default exports only there.
1.8. Do name prop types
Declare an exported XProps type right above the component and use it in the signature. Don't use React.FC or anonymous object types in the signature. This follows the Use named types and "Export every type" rules from our TypeScript guidelines.
1.9. Lint and format with ESLint
Use eslint-plugin-react-hooks (
rules-of-hooks,exhaustive-deps; version 6 and later also ships the React Compiler rules).create-next-appincludes it througheslint-config-next. On Vite projects, add it to the ESLint config yourself.Format with ESLint Stylistic (
@stylistic/eslint-plugin) instead of Prettier. One tool, one config, one--fixpass: formatting rules live next to the rest of the lint rules and there is no conflict between two formatters.
1.10. Make substantial compositions their own component
A composition of components becomes a named component as soon as it has enough substance, even if it is used only once. Signs of substance: it represents a concept you can name (UserCard, InvoiceSummary), it has its own state, handlers or data needs, or it is more than a few lines of nested JSX. Give it a name, place it at the nearest common ancestor of its consumers (generic UI primitives go straight to src/components/, see 3.5.1), type its props (see 1.8) and treat it like any other component: it gets its own file, its own tests and its own review.
Repetition is the second trigger: the same combination of components, wiring and props in more than one place is a component by definition. Don't copy-paste the JSX. Duplicated compositions drift apart, and every visual or behavioural change has to be hunted down in each copy.
Small one-off layout fragments with no name of their own stay inline.
2. General project organization and architecture
We follow a conventional src/ folder structure. Shared conventions, regardless of the framework:
One config file:
src/config.tsDeclare all app route paths at
src/routes.ts(see Route definitions)All non-route components under
components/(can be nested)All hooks under
hooks/(can expose Providers)One folder for each (external) service. For example
api/(for REST APIs),graphql/for GraphQL orauth/for authorization service. They can include type definitions, data transformations, clients or anything related to that service and communication with it.One folder for locales:
locales/The rest: utility functions, helpers, etc... under
lib/(keep it clean, please)
Routing is where the two flavours differ: Next.js uses the app/ folder, Vite SPAs use pages/ plus a Router.tsx.
2.1. Next.js project structure
There is no pages/ folder: the Pages Router is legacy. Only Next.js special files (page.tsx, layout.tsx, route.ts, etc.) sit directly in a segment folder. Segment-scoped code is colocated in private folders (_components/, _lib/, see 3.5.1); generic UI components and anything shared across unrelated areas go to components/. Component library primitives live in components/ui/: it's where the shadcn/ui CLI installs them, and we keep the same folder with other component libraries so the split between library primitives and our own components is always the same.
2.2. Vite SPA project structure
If using a repo for both api and client, put the above inside client/ folder
2.3. References (project structure)
Next.js project structure docs
Route definitions idea taken from Redwood framework
3. Description of the most common patterns used to solve common problems
3.1. State management
In general, prefer hooks over any other solution
Most "global state" is really server state. Keep it on the server and let the data layer cache it instead of copying it into a client store
If a user could want to share, bookmark or reload a view, its state belongs in the URL, not in a store
You probably don't need redux. Hooks, Context and zustand cover almost every case. If redux is unavoidable, use Redux Toolkit and its hooks API
3.1.1. Local state management
React hooks (useState, useReducer) are enough most of the time. Keep state as close as possible to the components that use it and lift it only when needed.
3.1.2. Global state management
Pick the simplest option that works, in this order:
Server-side state. Data that lives in a database or API belongs to the server. Read it in Server Components (Next.js) or with TanStack Query in client components, and mutate it with Server Actions or mutations. TanStack Query's cache is the "global store" for that data: don't duplicate it in a client store
URL state. Anything that describes which view the user is looking at goes in the URL: current page, page size, filters, search query, sort column and direction, active tab, open drawer or selected item. Links become shareable, the back button works, reloads keep the view and Server Components can read the params on the server. See URL state
React Context. For simple client-only state that rarely changes and is read by many components: theme, locale, current user, feature flags. Keep each context small and colocate the provider with the subtree that needs it
zustand. For complex client-only state: many writers, frequent updates, derived data or state that outlives a subtree (multi-step wizards, editors, carts). Prefer zustand over hand-rolled context plus reducers. Keep stores small and split them by domain
redux. Only when a project already depends on it. Use Redux Toolkit and its hooks API
Context is not a global store: every consumer re-renders when the value changes. If a context grows or updates often, move it to zustand.
3.1.3. URL state
The URL is the first place to put view state. Rules:
Search params hold view state (
?page=2&q=react&sort=-createdAt&status=open); the path holds identity (/posts/42). Never put secrets or large payloads in eitherParse and validate search params with zod in one place per route (
_lib/searchParams.tsin Next.js). Defaults live in the schema, not spread over the componentsOmit a param when it has its default value so canonical URLs stay short and cache-friendly
Reset dependent params together: changing a filter or the search query sends the user back to page 1
Read the params on the server whenever possible. In Next.js,
page.tsxreceivessearchParams(a Promise since Next.js 15): validate them and fetch in the Server Component. Client components that need to update them useuseSearchParamswithrouter.replace(orpushwhen the change should create a history entry)In a Vite SPA use react-router's
useSearchParamsplus the same zod schemaIf the project handles many params, use nuqs: typed,
useState-like search params with a shared parser for server and client. It works on Next.js App Router and react-router
3.2. External services
Create a clean interface for each service. For example:
src/api/index.ts(functions to send http requests to a REST API) orsrc/graphql/index.ts(queries and mutations of a GraphQL endpoint)Prefer generated types over hand-written ones: Hey API for REST, GraphQL Code Generator for GraphQL. Fall back to
<service-name>/types.tswhen there is no schema to generate fromUse declarative data fetching: prefer
useQueryoverfetch(available both in Apollo client and TanStack Query)
3.3. GraphQL
We currently use apollo-client
Generate types and code as much as possible with GraphQL Code Generator.
In general, keep all graphql related code inside graphql/ folder.
3.4. REST
Generate the client and its types from the OpenAPI spec with Hey API (
@hey-api/openapi-ts) intosrc/api/generated/. Its TanStack Query plugin produces ready-to-use query and mutation options. See our TypeScript guidelinesHand-write only
src/api/client.ts(base URL, auth headers, interceptors) and thin wrappers around the generated codeNo spec? Fall back to a single
src/api/index.tsexporting all API interactions andsrc/api/types.tsfor entity type definitionsIf Auth and API are different services, is common to have two folders (
src/authandsrc/api) and the API depends on authorization (JWT tokens, for example). If auth and API are in the same service, thesrc/authfolder can be omitted.
3.5. Routing
Next.js routes are defined by the file system (App Router). Vite SPAs use react-router. In both cases keep the route paths in src/routes.ts (see Route definitions).
3.5.1. Next.js App Router
Folders under
app/are URL segments.page.tsxis the route UI,layout.tsxwraps its children and persists across navigation[id]for dynamic segments,[...slug]for catch-all routes,(group)for route groups (shared layout, no URL segment)loading.tsx,error.tsxandnot-found.tsxare the Suspense, error boundary and 404 conventionsNavigate with
next/linkOnly Next.js special files (
page.tsx,layout.tsx,loading.tsx,error.tsx,not-found.tsx,route.ts) sit directly in a segment folder. Cross-cutting reusable components still go tocomponents/; code that belongs to one route segment is colocated inside the segment using private folders.
Private folders. Prefixing a folder with an underscore (_folder) opts it and all its children out of routing: the router never turns it into a URL segment, even if it contains files named like special files. Use them to colocate segment-scoped code without polluting the segment root:
_components/: UI components used only by that segment._lib/: logic: Server Actions, validation, search params parsing (see 3.1.3), helpers.Tests stay next to their source inside the same private folder.
Place shared code at the nearest common ancestor of its consumers: used by one route, that segment's private folders; used by several routes under a layout, the private folders of the segment that owns the layout; used across unrelated areas, src/components/ and src/lib/ as before. Exception: generic, purely UI components with no domain knowledge (a combobox, a modal, a button) skip this rule and always live in the root src/components/, even if only one segment uses them today. They are the project's design system, not segment code. Two notes: colocation in app/ is safe even without the underscore (only page.tsx and route.ts create URLs), so the prefix's value is signalling intent and protecting against future special-file collisions; and if a real URL segment must start with an underscore, name the folder %5FfolderName. Reference: Next.js docs, Project structure: private folders.
3.5.2. Route definitions
It's a file to generate route paths. Advantages:
You get an overview of all available routes on the app
It helps to prevent errors when declaring routes
Route completion via editor
In Next.js it keeps links in sync with the
app/folder structure
Usage:
3.5.3. Access route parameters
For a route like /posts/:postId/comments/:commentId:
3.5.4. SPA with Vite and react-router
Use react-router v7 with hooks. Install the
react-routerpackage:react-router-domis now a thin re-export kept for compatibilityA page is a component rendered from
Router.tsx. It can access route parameters and lives insrc/pages/(nested to reflect the URL structure)react-router v7 also has a framework mode (loaders, actions, SSR) that is the successor of Remix. It's a valid option for projects that don't fit Next.js
3.6. Server and client components
Only applies to Next.js. In a Vite SPA every component is a client component.
Every component under
app/is a Server Component by default: it can beasync, fetches data directly (database via Drizzle, API client) and has no hooks or event handlersAdd
"use client"only to the leaves that need state, effects or browser APIs. Keep the boundary as low in the tree as possibleData: Server Components fetch directly, client components use TanStack Query
Mutations: Server Actions (
"use server") withuseActionState. Keep react-hook-form for client-side validation UXReact 19:
use()reads promises and context,refis a normal prop (noforwardRef),<Context value={...}>renders as a providerReact Compiler: enable it (
reactCompiler: trueinnext.config.ts,babel-plugin-react-compileron Vite) and stop hand-writinguseMemo,useCallbackandmemounless profiling shows a need
3.7. Testing
Unit and component tests: Vitest with React Testing Library. Async Server Components are not supported by React Testing Library yet: cover them with end-to-end tests
End-to-end tests: Playwright
Favour end-to-end testing over component tests
More important to cover critical paths than general coverage
Ensure business logic are pure functions and write unit tests for them when needed
See also our Testing guidelines.
4. Libraries
4.1. Recommended libraries
Components: shadcn/ui (our default choice; copies the components into
components/ui/)Styling: tailwindcss (used by default in all our frontend projects)
Internationalization: react-intl, or next-intl on Next.js App Router
Forms: react-hook-form
Validation: zod (with
@hookform/resolversfor forms)Global state management: zustand
Server state / data fetching: TanStack Query (formerly react-query)
Tables: TanStack Table (headless; pair it with shadcn/ui's
Tableor theData Tablerecipe)Typed REST client: Hey API
Http: ky
Utilities: es-toolkit (modern, tree-shakeable lodash replacement; prefer it over lodash and over hand-written helpers in
lib/)GraphQL API: apollo-client
Routing: Next.js App Router, or react-router v7 for SPAs
Testing: Vitest, React Testing Library, Playwright
Lint and format: eslint-plugin-react-hooks, ESLint Stylistic
4.2. Other libraries we have used
Components (we prefer shadcn/ui, see 4.1; these appear in existing projects or when a client requires them)
Hooks:
4.3. Libraries worth taking a look into
State management
Component library
5. Learning resources
React docs are quite good. Recommended reading: https://react.dev/learn
Next.js official course: https://nextjs.org/learn
egghead.io is one of our favourite places to learn and Kent C. Dodds is a master, so this can't fail: https://egghead.io/courses/the-beginner-s-guide-to-react
Last updated
