Project Setup Commands
# Framework path — what react.dev itself recommends starting from
npx create-next-app@latest my-app --typescript
cd my-app
npm run dev
# Plain-Vite path — no framework, just React + a build tool. What every other
# starter on this page assumes, since this cheat sheet covers React itself,
# not a specific framework's routing/data-fetching APIs.
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
# Build for production, then preview that build locally
npm run build
npm run preview
A couple of commonly-needed extras on top of the plain-Vite path:
# Client-side routing (framework-free — Next.js has its own router built in)
npm install react-router-dom
# Fetching/caching server state (an alternative to hand-rolled useFetch hooks)
npm install @tanstack/react-query
If you’re building a real product, start with the framework path — react.dev
is explicit that it’s the recommended default now, since it solves routing,
data fetching, and server rendering you’d otherwise have to wire up by hand.
The Vite path is what this page’s other starters assume, since they’re
about React itself rather than any one framework’s conventions.
React's own official guidance and the plain-Vite alternative — which to reach for depends on whether you need a full framework or not.
- react.dev recommends a framework
- Next.js is the officially recommended starting point for a new production React app — it handles routing, data fetching, and server rendering for you
- npm create vite@latest
- The right choice for a plain client-side app with no framework — learning React, a widget, or an SPA that doesn't need server rendering
- npm run dev
- Starts the local dev server with hot module replacement (both paths below)
Function Component Starter
interface CardProps {
title: string;
onSelect?: () => void;
}
function Card({ title, onSelect }: CardProps) {
return (
<div className="card" onClick={onSelect}>
<h3>{title}</h3>
</div>
);
}
export default Card;
Copy this, rename Card/CardProps, and fill in the markup — this is the
skeleton for the large majority of components that just take some data and
render it, with no internal state of their own.
The shape almost every new component file starts from — typed props, destructured, one default export.
- Props interface
- Named after the component, e.g. CardProps — keeps prop types readable in editor tooltips
- Optional prop (?)
- Use ? for props a caller can omit, and give it sensible fallback behavior in the body
- Default export
- One component per file convention — makes the file easy to find by its export
Component with Children Starter
interface PanelProps {
title: string;
children: React.ReactNode;
}
function Panel({ title, children }: PanelProps) {
return (
<section className="panel">
<h3>{title}</h3>
<div className="panel-body">{children}</div>
</section>
);
}
export default Panel;
The difference from a plain component starter is just the children prop
— typed as React.ReactNode rather than a specific value — which lets the
caller pass any markup through instead of a fixed set of props.
For a component that wraps arbitrary markup a caller provides — a panel, a card, a layout shell.
- React.ReactNode
- The type for children — covers elements, strings, numbers, fragments, and null/undefined
- Nesting
- Called as <Panel title="Notes">...content...</Panel> — whatever is between the tags becomes children
Component with Local State Starter
import { useState } from 'react';
interface CounterProps {
initialCount?: number;
}
function Counter({ initialCount = 0 }: CounterProps) {
const [count, setCount] = useState(initialCount);
return (
<button onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
);
}
export default Counter;
This is the same shape as the plain component starter with one useState
call added — copy it as the starting point any time a component needs to
remember something between renders that isn’t passed in from a parent.
For a component that manages its own value — a counter, a toggle, a small piece of form state.
- Default via destructuring
- initialCount = 0 supplies a fallback when the caller omits the prop entirely
- Functional update
- setCount((c) => c + 1) reads the latest value instead of a possibly-stale closure
forwardRef Component Starter
import { forwardRef } from 'react';
interface TextInputProps {
label: string;
}
const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label }, ref) => (
<label>
{label}
<input ref={ref} />
</label>
)
);
TextInput.displayName = 'TextInput';
export default TextInput;
A parent can now do <TextInput ref={inputRef} label="Name" /> and call
inputRef.current.focus() — without forwardRef, the ref prop would be
silently dropped rather than reaching the underlying <input>.
For a component whose underlying DOM node a parent needs direct access to — a custom input, a focusable widget.
- forwardRef<T, P>
- First type parameter is the ref target (the DOM node type), second is the props type
- displayName
- Set explicitly — forwardRef-wrapped components otherwise show up unnamed in DevTools
- When to use it
- Only when a parent genuinely needs the DOM node itself — most components never need this
Custom Hook Starter
import { useState, useCallback } from 'react';
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
return [value, toggle] as const;
}
export default useToggle;
Used as const [isOpen, toggleOpen] = useToggle(); — copy this shape for
any bit of stateful logic (a toggle, a fetch, a subscription) that more
than one component needs, and rename useToggle/value/toggle to fit.
The shape a new custom hook starts from — internal state, a stable API, one clear return value.
- use prefix
- Required, not just convention — it is how React and lint rules recognize a function as a hook
- as const
- Locks the return tuple's element types/order, so callers get [value, toggle] instead of a loose array
- useCallback
- Keeps the returned function reference stable across renders, so it is safe to use as a dependency
Context + Provider + Hook Starter
import { createContext, useContext, useState, type ReactNode } from 'react';
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => setTheme((t) => (t === 'light' ? 'dark' : 'light'));
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider');
return ctx;
}
Wrap the subtree once in <ThemeProvider>, then any component inside it
just calls const { theme, toggleTheme } = useTheme(); — no prop drilling,
and the guard clause catches a missing provider immediately instead of a
confusing undefined further down the tree.
The full pattern for sharing a value across a subtree — a context, its provider, and a hook that reads it safely.
- Undefined default
- createContext<T | undefined>(undefined) — forces every consumer through the hook, never the raw context
- Guard clause
- Throwing when the hook is called outside its provider turns a silent bug into an immediate, clear error
- value memoization
- Wrap the provider value in useMemo if it is an object, so consumers only re-render when it actually changes
Data-Fetching Hook Starter
import { useState, useEffect } from 'react';
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then((json: T) => setData(json))
.catch((err) => {
if (err.name !== 'AbortError') setError(err);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, error, loading };
}
export default useFetch;
Used as const { data, error, loading } = useFetch<User[]>('/api/users');
— copy this shape for any request, and swap fetch for a project’s own API
client if it has one; the state shape and cleanup stay the same.
A custom hook covering the three states every fetch needs — loading, error, and data — with cleanup on unmount.
- Three-state shape
- { data, error, loading } — lets the calling component render each case explicitly
- AbortController
- Cancels the in-flight request on cleanup, so a fast unmount or url change never sets state on a dead component
- Dependency on url
- Re-runs the fetch whenever the url argument changes, exactly like any other useEffect
Error Boundary Starter
import { Component, type ReactNode } from 'react';
interface Props {
fallback: ReactNode;
children: ReactNode;
}
interface State {
hasError: boolean;
}
class ErrorBoundary extends Component<Props, State> {
state: State = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error) {
console.error('ErrorBoundary caught:', error);
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children;
}
}
export default ErrorBoundary;
Used as <ErrorBoundary fallback={<p>Something went wrong.</p>}>...</ErrorBoundary>
around a specific widget or route — this is still a class component because
React has no hook equivalent for catching render errors in a subtree.
The one component type hooks still cannot replace — catches render errors in its subtree and shows a fallback instead of a blank screen.
- getDerivedStateFromError
- A static method that turns a caught error into fallback-render state — required, and cannot be a hook
- componentDidCatch
- Where to log the error (to a monitoring service, console, etc.) — separate from deciding what to render
- Scope it tightly
- Wrap one risky subtree (a widget, a route), not the whole app — so one failure does not blank everything
Compound Component Starter
import { createContext, useContext, useState, type ReactNode } from 'react';
const TabsContext = createContext<{ active: string; setActive: (id: string) => void } | undefined>(undefined);
function Tabs({ defaultTab, children }: { defaultTab: string; children: ReactNode }) {
const [active, setActive] = useState(defaultTab);
return <TabsContext.Provider value={{ active, setActive }}>{children}</TabsContext.Provider>;
}
function Tab({ id, children }: { id: string; children: ReactNode }) {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error('Tabs.Tab must be used within <Tabs>');
return (
<button aria-pressed={ctx.active === id} onClick={() => ctx.setActive(id)}>
{children}
</button>
);
}
Tabs.Tab = Tab;
export default Tabs;
Used as <Tabs defaultTab="a"><Tabs.Tab id="a">First</Tabs.Tab><Tabs.Tab id="b">Second</Tabs.Tab></Tabs>
— the same context-plus-hook idea as the standalone context starter, just
scoped to one component family instead of a whole subtree.
A component family that shares implicit state via context, so callers compose it like <Tabs><Tabs.Tab /></Tabs> instead of passing config props.
- Attached sub-components
- Tabs.Tab is just Tab assigned onto the Tabs function — no extra module or export needed
- Implicit shared state
- The active tab lives in context, so Tab never needs an index prop wired up by the caller
- Composable markup
- Callers control order, spacing, and what sits between tabs — a fixed-props API could not offer that
Lazy-Loaded Component Starter
import { lazy, Suspense } from 'react';
const ChartPanel = lazy(() => import('./ChartPanel'));
function Dashboard() {
return (
<Suspense fallback={<p>Loading chart…</p>}>
<ChartPanel />
</Suspense>
);
}
export default Dashboard;
ChartPanel’s code is not downloaded until Dashboard actually renders it
— reach for this for anything large and not needed on first paint (a
chart library, a modal’s contents, a rarely-visited settings panel).
Splits a heavy component into its own chunk, loaded only when it is actually rendered.
- React.lazy
- Takes a dynamic import() and returns a component that resolves it on first render
- Suspense fallback
- Required wrapper — shown while the chunk is downloading, not just on slow networks
- Default export only
- React.lazy expects the imported module to have a default export