# React Principles — Cookbook

> Production-grade React patterns and principles. A curated curriculum for modern React development covering folder structure, TypeScript, components, state, forms, services, and more.

This is the **compact** version of the cookbook — principle statements and rules only, no code examples. For full content including code examples and framework-specific implementations, see:

- Full version: https://reactprinciples.dev/llms-full.txt
- Interactive cookbook: https://reactprinciples.dev/cookbook

Stack: Next.js 16, React 19, TypeScript 5, Tailwind CSS v4, TanStack Query v5, Zustand v5, React Hook Form + Zod.

---


## Foundations

### Folder Structure

> A feature-based folder structure so you always know where a file goes — and why it belongs there.

**Principle:** A good folder structure answers one question instantly: 'where does this file go?' Feature-based organization groups everything related to a feature together — its components, hooks, and data — so you spend time building, not searching. When a feature grows or gets deleted, everything moves together. This works best for apps with multiple distinct features and more than one developer — think e-commerce with products, cart, checkout, and auth all living side by side. For a small app with 2–3 pages, this structure is like organizing a studio apartment with a full filing cabinet system. Useful later, overkill now.

**Tip:** One rule to decide where a file goes: if only one feature uses it, put it in that feature. If two or more features need it, move it to shared/. If it's infrastructure (API client, query config), put it in lib/.

**Conventions:**
- **Feature-based grouping** — Everything related to a feature lives in src/features/[name]/ — its components, hooks, and stores together. The stores/ directory is only needed when the feature has shared UI state that multiple components within that feature need — like a multi-step form or a selected item. If all data comes from an API, skip the store.
- **Co-location** — Files live next to the code they describe — a component's types go in the same file, a feature's types go in that feature folder. A shared/ types folder is fine only for types used by two or more features. The decision is based on scope, not file type.
- **No cross-feature imports** — By convention, features avoid importing directly from each other. Code needed by multiple features moves to src/shared/. Cross-feature imports are acceptable when composing product surfaces — for example, a layout feature pulling in a ThemeToggle from another feature — but should not be the default.
- **Public API via index.ts** — By convention, each feature exposes its public API through an index.ts barrel file. Other parts of the codebase import from the feature, not from its internals. This keeps refactoring contained — if a file moves inside the feature, nothing outside breaks. If you want to enforce this automatically, ESLint's no-restricted-imports rule can prevent direct internal imports.

Read more: https://reactprinciples.dev/cookbook/folder-structure

### TypeScript for React

> How to type component props, event handlers, and hooks correctly. The contracts that prevent silent bugs.

**Principle:** Bugs caught at compile time cost nothing to fix. Bugs caught in production cost everything. TypeScript for React is not about learning the full TypeScript language — it is about writing the right contracts between your components so that mistakes are caught before the code even runs.

**Tip:** Start by typing your component props. If you can describe what a component accepts and returns, the rest of the types follow naturally.

**Rules:**
- **interface for component props** — Use interface to define component props. It is extendable and reads clearly as a contract.
- **type for unions and utilities** — Use type for union types, utility types, and function signatures — things that are not directly 'objects with fields'.
- **Never use any** — any disables type checking completely. Use unknown and narrow it with type guards instead.
- **strict: true in tsconfig** — Strict mode enables the full set of type checks. Without it, TypeScript catches only the most obvious errors.

Read more: https://reactprinciples.dev/cookbook/typescript-for-react

### Component Anatomy

> The consistent internal structure every component follows — imports, types, constants, function, export.

**Principle:** When every component follows the same structure, you stop thinking about where things go inside a file and start thinking about what the component actually does. Consistent anatomy means anyone on the team can open any file and immediately know where to look — props are always at the top, constants are always before the function, exports are always at the bottom.

**Tip:** The hardest part of component anatomy is constants vs. props. Rule of thumb: if it never changes based on what's passed in, it is a constant. If it could change from outside, it is a prop.

**Rules:**
- **Imports first** — Order: React → external libraries → internal aliases (@/) → relative imports (./). This makes dependencies visible at a glance.
- **Types and interfaces second** — Define all types used in this file immediately after imports. Props interface always comes first.
- **Constants third** — Component-scoped constants (static data, config, labels) come before the function. Never define constants inside the function body.
- **Component function fourth** — The function itself comes after everything it depends on. Keep it focused — if it grows past 200 lines, split it.
- **Named export last** — Always use named exports, never default exports. Named exports make refactoring and search easier.

Read more: https://reactprinciples.dev/cookbook/component-anatomy

### useEffect & Render Cycle

> When effects run, why the dependency array exists, and how to clean up after yourself.

**Principle:** useEffect is not a lifecycle method — it is a synchronization tool. It answers one question: 'what side effects need to stay in sync with this data?' Every time the dependency array changes, React re-runs the effect to keep things synchronized. When you understand this mental model, dependency arrays stop feeling like magic rules and start making sense.

**Tip:** If you find yourself writing useEffect to fetch data, stop. That is what React Query is for. useEffect is for synchronizing with things outside React — browser APIs, subscriptions, timers.

**Rules:**
- **Always declare dependencies honestly** — Every value from the component scope used inside the effect belongs in the dependency array. If you add eslint-disable to hide a missing dependency, you have a bug.
- **Return a cleanup function when needed** — If your effect creates a subscription, timer, or event listener — clean it up in the return function. Otherwise you get memory leaks and stale handlers.
- **Empty array means once on mount** — [] runs the effect once after the first render. Only use this when the effect truly has no dependencies — not as a shortcut to avoid thinking about deps.
- **Do not use useEffect for data fetching** — Fetching data inside useEffect causes race conditions, no loading state management, and no caching. Use React Query instead.

Read more: https://reactprinciples.dev/cookbook/useeffect-render-cycle

### Component Composition

> How components combine and communicate — children props, slot patterns, and why composition beats deep prop drilling.

**Principle:** Prop drilling happens when you pass data through multiple components that do not use it — just to get it to a component deep in the tree. Composition solves this differently: instead of passing data down, you pass components down. The parent controls what gets rendered, and children receive exactly what they need directly.

**Tip:** When you find yourself adding a prop to a component just to pass it further down, stop. That is the signal to use composition instead.

**Rules:**
- **Use children for flexible content** — The children prop lets a parent inject content into a component without the component needing to know what it is.
- **Use named slots for multiple injection points** — When you need more than one place to inject content (header + footer + body), use named props instead of children.
- **Prefer composition over configuration** — A component that accepts children is more flexible than one with 10 props controlling its internals. Compose behavior, do not configure it.
- **Keep components focused** — Each component does one thing. Composition is how you build complex UIs from simple, focused pieces.

Read more: https://reactprinciples.dev/cookbook/component-composition

### Custom Hooks

> The boundary between logic and rendering. When to extract a hook, what the rules are, and how to avoid the most common mistake.

**Principle:** A custom hook is not just a function that starts with 'use' — it is a boundary between logic and rendering. The component handles what the user sees. The hook handles how data gets there. When you separate these two concerns, components become easier to read, logic becomes easier to test, and both become easier to change independently.

**Tip:** If you would write a unit test for the logic, it belongs in a hook. If you would write a component test for it, it belongs in the JSX.

**Rules:**
- **Name starts with 'use'** — This is not just a convention — React uses it to enforce the rules of hooks. A function starting with 'use' is treated as a hook.
- **Extract when logic repeats** — If the same stateful logic appears in two components, extract it to a hook. Do not copy-paste hooks between components.
- **Extract when logic is complex** — If a component has more than one useEffect, multiple useState calls, or complex derived state — that logic belongs in a hook.
- **Hooks are not global state** — Each component that calls a hook gets its own isolated instance. Hooks do not share state between components unless backed by a store or context.

Read more: https://reactprinciples.dev/cookbook/custom-hooks

### Services Layer

> How to organize all backend communication in one place — so when an API changes, you fix it in one file, not twenty.

**Principle:** When you fetch data directly inside a component, the component becomes responsible for knowing the URL, the HTTP method, the request format, and the error handling. That is four responsibilities too many. A services layer centralizes all backend communication — components just call a function and get data back. When the API changes, you fix it in one file, not twenty.

**Tip:** A service function should read like plain English: getUserById(id), createOrder(data), deletePost(id). If it needs more than one argument object, consider splitting it into two functions.

**Rules:**
- **Services only talk to the API** — A service function takes inputs, calls the API, and returns data. It does not touch state, does not render anything, and does not know about React.
- **One file per resource** — Group service functions by the API resource they belong to: users.ts, orders.ts, recipes.ts. Not by HTTP method.
- **Services live in lib/** — The services layer belongs in src/lib/ alongside the API client and query keys — not inside a feature folder.
- **Hooks consume services, components consume hooks** — Components never call service functions directly. The chain is: service → custom hook → component.

Read more: https://reactprinciples.dev/cookbook/services-layer

### State Taxonomy

> Three categories of state — local, shared, and server — and exactly which tool handles each one.

**Principle:** Not all state is the same. Before reaching for any state management library, ask one question: where does this data come from? Local state lives inside one component. Shared state is UI state needed by multiple components. Server state comes from an API and has its own lifecycle — loading, error, stale, and needs refreshing. Each category has a different tool, and mixing them up causes bugs that are hard to trace.

**Tip:** When you find yourself putting API data into Zustand, stop. Server state belongs in React Query. When you find yourself using React Query for a toggle or a modal, stop. UI state belongs in useState or Zustand.

**Rules:**
- **Local state: useState** — If only one component needs it, keep it local. A form input value, a toggle, a hover state — these are all local state.
- **Shared state: Zustand** — If multiple components need the same UI state — sidebar open/closed, active theme, search dialog open — use Zustand. This is not server data.
- **Server state: React Query** — If it comes from an API, it is server state. React Query handles caching, background refetching, loading states, and error states automatically.
- **Never put server state in Zustand** — Storing API data in Zustand means you manage caching, staleness, and loading manually. React Query already does this — use the right tool.

Read more: https://reactprinciples.dev/cookbook/state-taxonomy


## Patterns

### Server State with React Query

> Fetch, cache, and synchronize server data using TanStack Query v5. Covers pagination, search, background refetching, and loading states.

**Principle:** Server state is async, shared, and can become stale. TanStack Query owns the entire lifecycle — fetching, caching, deduplication, and background revalidation. Components declare what data they need via custom hooks and stay completely free of fetch logic.

**Tip:** Never mirror server data into useState. If it came from an API, it belongs in React Query's cache. Local state is only for UI — modals, toggles, input values.

**Rules:**
- **Hooks own the fetching** — Query hooks go in hooks/queries/. Components only call the hook and render the result.
- **Hierarchical query keys** — Structure keys as arrays: ["users", "list", { search, page }] for granular cache invalidation.
- **Always set staleTime** — Default staleTime is 0 — every render refetches. Be explicit: 5 min for lists, 10 min for details.
- **Handle all states** — Always render isLoading, isError, and empty states. Never assume data exists on first render.

Read more: https://reactprinciples.dev/cookbook/server-state

### Client State with Zustand

> Manage global UI state across multiple Zustand stores. Covers selectors, actions, computed selectors, reset, and the 'use client' boundary in Next.js.

**Principle:** Client state — UI toggles, filter state, user preferences — belongs in Zustand, not React Query. Each store owns one domain. Components read a slice of state via selectors and call actions. No prop drilling, no context boilerplate.

**Tip:** One store per feature domain. Never put server state (API data) in Zustand — if it comes from an endpoint, it belongs in React Query.

**Rules:**
- **One store per domain** — useAppStore for app-wide settings, useFilterStore for filters, useSearchStore for search UI. Never mix concerns in a single store.
- **Actions inside the store** — Mutations happen in store actions, not in component event handlers. Keeps logic close to state.
- **Selectors over full-state** — Pass selector functions: useAppStore(s => s.theme) not useAppStore(). For multiple values, use useShallow from zustand/shallow.
- **Reset is first-class** — Always define a reset() action for stores that can be cleared. Useful for logout, navigation, and testing.
- **'use client' on the store file** — Zustand hooks call React internals (useState, useSyncExternalStore). Put 'use client' on the store file itself — never on barrel exports — so Server Components can still import types.

Read more: https://reactprinciples.dev/cookbook/client-state

### Form Validation with Zod

> Schema-first form validation with React Hook Form and Zod. Type-safe, declarative error messages, and zero boilerplate for create and edit flows.

**Principle:** The Zod schema is the single source of truth — it defines the shape, types, and error messages. React Hook Form handles registration, submission, and field state. Components never write validation logic; they display what the schema declares.

**Tip:** Write the schema before a single input. Share schemas across forms with .pick(), .extend(), or .omit(). Keep all error messages inside the schema, not in JSX.

**Rules:**
- **Schema before form** — Define the Zod schema first. Never add validation inline with register options or manual if-statements.
- **Omit server-generated fields** — Use .omit({ id: true, createdAt: true }) for create forms. The schema reflects what the user provides.
- **handleSubmit owns errors** — Wrap mutation calls in handleSubmit. Validation errors surface automatically without try/catch in the component.
- **Reset after success** — Call reset() after a successful mutation to clear all field values and dirty state.
- **Share schemas between create and edit** — Define a base schema, then derive create and edit variants with .omit() or .partial(). Single source of truth for all validation rules.

Read more: https://reactprinciples.dev/cookbook/form-validation

### Data Tables with TanStack Table

> Headless, sortable, filterable, and paginated tables using TanStack Table v8. Full styling control with no component library lock-in.

**Principle:** TanStack Table is a headless engine — it computes row models, manages sorting, filtering, and pagination state, but renders nothing. You own the markup. This separation means complete styling control without fighting a component library.

**Tip:** Wrap column definitions in useMemo with an empty dependency array. Column definitions are stable references — recreating them on every render causes unnecessary row model recalculations.

**Rules:**
- **Columns are stable** — Wrap column definitions in useMemo(() => [...], []). Redefining them each render triggers unnecessary re-sorts and re-filters.
- **Own the render loop** — Use flexRender() for both headers and cells. Never manually extract cell values — let the column definition handle rendering.
- **Server-side for large data** — Client-side filtering and sorting works up to ~1,000 rows. Beyond that, move pagination and filtering to the server.
- **Global vs column filters** — Use globalFilter for quick full-text search. Use column-level filters for advanced filtering UI with per-field controls.

Read more: https://reactprinciples.dev/cookbook/data-tables


## API Integration

### API Integration

> A custom fetch-based API client factory with typed methods, centralized error handling, and optional auth — no Axios needed.

**Principle:** All API calls flow through a single typed client created by createApiClient(). The factory configures the base URL, auth headers, and error handling once. Services wrap the client with domain-specific methods. React Query hooks wrap services for caching. Components never call fetch() directly.

**Tip:** The native Fetch API covers most use cases without a library. createApiClient adds type safety, automatic JSON serialization, query parameter handling, and centralized error handling — the exact gaps fetch leaves open — without pulling in Axios as a dependency.

**Rules:**
- **Single API Client Instance** — Create one instance via createApiClient() in lib/api.ts and import it everywhere. All requests share the same base URL, headers, and error handler.
- **Type All Responses** — Pass a generic type to every api.get<T>() call. TypeScript interfaces define the contract — if the backend changes shape, the compiler catches it.
- **Centralized Error Handling** — Pass an onError callback to createApiClient(). It fires on every failed request — connect it to a toast or error reporting service. Components never parse error responses.
- **Service → Hook → Component** — Services handle HTTP calls. Hooks wrap services with React Query. Components consume hooks. Each layer has one job — when the API changes, only the service file changes.

Read more: https://reactprinciples.dev/cookbook/api-integration

---

## More

- Compact cookbook (no code): https://reactprinciples.dev/llms.txt
- Full cookbook (with code): https://reactprinciples.dev/llms-full.txt
- Interactive web version: https://reactprinciples.dev/cookbook
- UI Kit components: https://reactprinciples.dev/docs
- AI skills (Claude/Cursor/Copilot): https://github.com/sindev08/react-principles-skills
