React Fundamentals Best Practices
Top 20 Basic Fundamentals
The essential building blocks every React basic learner should know before diving into advanced patterns.
- Components are functions that return JSX: A React component is just a JavaScript/TypeScript function that returns markup -- the function name must start with a capital letter.
- JSX is not HTML: JSX looks like HTML but compiles to JavaScript -- use
classNameinstead ofclass,htmlForinstead offor, and camelCase for event attributes likeonClick. - Curly braces embed expressions: Use
{variable}inside JSX to display dynamic values -- you can put any JavaScript expression inside, but not statements likeiforfor. - Props pass data downward: Parent components send data to children via props -- think of them as function arguments that flow in one direction, from parent to child.
- Props are read-only: Never modify a prop directly -- if a child needs to change something, the parent should pass down a setter function instead.
- useState adds interactivity: Call
useState(initialValue)to get a[value, setValue]pair -- updating state with the setter triggers a re-render. - Events use camelCase handlers: Attach event handlers like
onClick={handleClick}-- pass the function reference, never call it with parentheses likeonClick={handleClick()}. - Lists need a key prop: When rendering arrays with
.map(), every element needs a uniquekeyso React can efficiently track additions, removals, and reorders. - Conditional rendering uses ternaries or &&: Show or hide elements with
{condition ? <A /> : <B />}or{condition && <A />}since you cannot useifstatements inside JSX directly. - Controlled inputs pair value with onChange: Set
value={state}andonChange={e => setState(e.target.value)}together -- settingvaluealone makes the input read-only. - children is a special prop: Whatever you place between a component's opening and closing tags becomes
children-- this is how you build wrapper and layout components. - useEffect runs side effects: Use
useEffect(() => { ... }, [deps])for things that happen outside rendering like API calls, timers, or subscriptions. - The dependency array controls when effects re-run: An empty array
[]means "run once on mount" -- listing variables means "re-run when these change" -- omitting it means "run after every render." - import and export connect files: Use
export defaultorexportto make a component available, thenimportit in another file to use it -- the@/alias points to your project root. - Fragments avoid extra DOM nodes: Wrap sibling elements in
<>...</>(Fragment) when you don't need an extra<div>-- this keeps your rendered HTML clean. - Inline styles use JavaScript objects: Write
style={{ color: "red", fontSize: "16px" }}with camelCase properties -- but in practice, use a CSS framework like Tailwind instead. - Lift state to the nearest common parent: When two sibling components need the same data, move the state up to their parent and pass it down as props.
- Derive values instead of syncing state: If a value can be computed from existing state or props, calculate it during render instead of storing it in a separate
useState. - TypeScript catches errors before they reach users: Type your props with
interfaceortypeand use.tsxfiles -- the compiler will flag missing props, wrong types, and typos instantly. - Keep components small and focused: Each component should do one thing well -- if a component is getting long, extract parts of it into smaller child components.
Top 25 Intermediate Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
- Never Define Components Inside Components: A component declared inside another component gets a fresh identity on every parent render, so React unmounts and remounts it each time — destroying local state and effects; always hoist components to module scope:
function Parent() { function Child() {} /* bad — Child remounts every render */ }. - Treat Props as Immutable: Mutating a prop (e.g.,
props.items.push(x)) quietly mutates the parent's data and breaks React's rendering model; copy into new arrays or objects before modifying and let the parent own the state. - Extend Native Props With ComponentPropsWithoutRef: Type wrapper components with
React.ComponentPropsWithoutRef<"button">so every native attribute stays in sync with the DOM API; re-declaring props manually drifts the moment the DOM spec changes. - Prefer interface for Props:
interfacegives better error messages and supports declaration merging, so use it for component prop shapes; reservetypefor unions, mapped types, and other transformations that interfaces cannot express. - Guard Against 0 in && Conditionals:
{count && <Badge />}renders the literal string "0" whencountis 0 because 0 is falsy but still renderable; usecount > 0 && <Badge />or a ternary to avoid the infamous "0" bug. - Discriminated Unions for Status: Model state as
{ status: "success"; data } | { status: "error"; error }so TypeScript narrows inside each branch; this eliminates optional-chaining noise and makes unreachable branches obvious. - Switching Element Types Resets State: Changing a component type at the same tree position (e.g.,
<Spinner />→<DataTable />, or<input>↔<textarea>) unmounts the old subtree and wipes its state; keep the same type and toggle props when you want state to persist. - Pass Handlers, Not Invocations:
onClick={handleClick}attaches the function;onClick={handleClick()}calls it during render and wires the return value as the handler, which is almost never what you want — wrap in an arrow when you need to inject arguments:onClick={() => handleClick(id)}. - onChange Fires Every Keystroke: React's
onChangemaps to the nativeinputevent, not nativechange, so it fires on every keystroke instead of on blur — useonBlurif you actually want "commit on blur" semantics. - Use currentTarget, Not target:
e.targetis the element the event originated on (which may be a child), whilee.currentTargetis the element the handler is attached to; read attributes you control fromcurrentTargetto avoid surprises when clicking nested elements. - Escape to addEventListener When Needed: JSX handler props do not support
{ passive: false }orwindow/document-level events; for Escape-key listeners, scroll monitors, ortouchmoveneedingpreventDefault, useuseEffect+addEventListeneron a ref and return a cleanup. - Controlled Inputs Need value + onChange: Setting
valuewithout a matchingonChangemakes the input read-only because React pins the DOM value to state; either pair the two —<input value={name} onChange={e => setName(e.target.value)} />— or switch todefaultValuefor an uncontrolled input. - Checkboxes and Radios Use checked: Checkbox and radio inputs consume
checked/defaultChecked, notvalue/defaultValue; mixing them up silently does the wrong thing sincevalueon a checkbox is the submitted token, not the checked state:<input type="checkbox" checked={on} onChange={e => setOn(e.target.checked)} />. - Call useFormStatus in a Child:
useFormStatus()only returns accurate pending info when called from inside a descendant of the<form>; calling it in the same component that renders the form returns stale defaults with no error, so extract the submit button into its own component. - Parse input.value Strings:
e.target.valueis always a string — even for<input type="number">— so wrap it withNumber(...)orparseInt(...)before using it in math or storing typed state:onChange={e => setAge(Number(e.target.value))}. - Use className, Not class: JSX compiles to
React.createElement, so HTML'sclassattribute isclassName,forishtmlFor, and styles take a camelCase object; using the HTML names compiles but warns and can silently drop styling in strict environments. - Objects Are Not Valid Children: Rendering
{user}throws "Objects are not valid as a React child"; render a specific field like{user.name}orJSON.stringify(user)for debug output, and remember that0renders as text butnull/false/undefinedrender nothing. - ReactNode for children, JSX.Element for Returns: Type
childrenasReact.ReactNode(which covers JSX, strings, numbers, arrays,null) and reserveReact.JSX.Elementfor return types that always return a single JSX element. - Keys Unique Among Siblings: A list's
keymust be unique among its siblings (not globally), and it must be stable across renders — useitem.id, notMath.random()orDate.now(), or React will remount every row and destroy its state. - Avoid Index Keys on Dynamic Lists: Using
indexaskeyon lists that reorder, filter, or insert causes React to reuse the wrong DOM nodes, producing stale input values and broken focus; index keys are only safe for truly static lists. - Use key to Force Remount: Changing the
keyon a component is the canonical way to reset all of its internal state —<PlayerProfile key={currentPlayerId} />deliberately remounts when the player changes, which is a feature, not a hack. - Short Fragments Can't Take key: The
<>...</>shorthand does not accept akeyprop; when mapping fragments, importFragmentand use<Fragment key={...}>...</Fragment>to avoid duplicate-key warnings or unkeyed renders. - ref.current Doesn't Trigger Renders: Updating
ref.currentdoes not re-render the component — that is the point; reach foruseRefwhen the UI does not depend on the value (timer IDs, observers, latest-value cache) anduseStatewhen it does. - ref Is a Regular Prop in React 19:
forwardRefis deprecated; just typerefas an ordinary prop withReact.Ref<HTMLInputElement>and destructure it alongside the others — and remember{...props}now spreadsreftoo, so pull it off explicitly when passing through. - Return Cleanup From Ref Callbacks: React 19 ref callbacks can return a cleanup function like
useEffect, so wire observers asref={node => { if (!node) return; const obs = new ResizeObserver(...); obs.observe(node); return () => obs.disconnect(); }}instead of the old null-check dance.