Best practices in React aren't about style preference — they determine whether a codebase stays easy to change as it grows. This lesson covers single-responsibility components, state colocation, disciplined useEffect usage, immutability, and naming conventions that hold up across a real team.
1Why Best Practices Matter More at Scale
A component that does everything is manageable in a small demo but becomes a liability once dozens of engineers depend on it. Best practices exist to keep a codebase changeable over time — the cost of skipping them compounds as more people and more features touch the same code.
2Single Responsibility Components
Each component should represent one clear piece of UI or manage one specific concern. A component that fetches data, transforms it, and renders several unrelated widgets should be split into focused pieces — smaller components are easier to name accurately, test in isolation, and reuse in other contexts.
3Colocate State Close to Where It's Used
State should live at the lowest level of the component tree that actually needs it. Lifting state further up than necessary — 'just in case' — adds indirection, forces unrelated components to re-render, and makes the data flow harder to trace back to its source.
4Don't Overuse useEffect
useEffect exists to synchronize a component with something outside React, like a subscription or a manual DOM interaction — not to compute a value derived from existing props or state. If a value can be calculated directly during render, calculating it in an effect and storing it in extra state just adds an unnecessary re-render and a source of bugs.
5Never Mutate State or Props
React relies on reference comparison, not deep equality checks, to detect changes. Mutating an existing array or object in place leaves its reference unchanged, which can cause React to skip a re-render it should have triggered, and silently breaks any memoization relying on that reference. New state should always be a new object or array.
6Meaningful Names, Not Generic Ones
Components, props, and variables should be named for what they represent, not their position in the file or their type. Descriptive boolean prop names like isLoading or hasError communicate intent at the call site, while vague names force every reader to trace back through the implementation to understand what a value means.
7Step-by-Step Breakdown
Why Best Practices Matter More at Scale. A single component doing everything is fine for a demo, but a real codebase has dozens of engineers touching hundreds of components. Best practices aren't stylistic preferences — they're the difference between a codebase that stays easy to change and one that becomes too risky to touch after a year of growth.
Single Responsibility Components. Each component should do one thing: render one clear piece of UI, or manage one specific concern. If a component fetches data, transforms it, formats dates, and renders three unrelated widgets, split it. Small components are easier to name well, test in isolation, and reuse elsewhere.
Colocate State Close to Where It's Used. Don't lift state higher than it needs to be. If only one component reads and writes a piece of state, keep it local to that component. Lifting state up is for sharing data between siblings — lifting it further than necessary just adds indirection and causes unrelated parts of the tree to re-render.
A modal's open/closed state is only ever read and set inside the Modal component itself. Where should that state live?
- →Locally, inside the Modal component
- →In a global store like Redux, just in case
Don't Overuse useEffect. useEffect is for synchronizing with something outside React — a subscription, a DOM API, a network request. It is not for computing a value derived from props or state; that should just be a plain calculation during render. Reaching for useEffect to 'react' to a state change usually means state should be derived instead.
Never Mutate State or Props. React detects changes by comparing references, not deep-inspecting values. Mutating an array or object in place — arr.push(x), obj.key = y — leaves the reference identical, so React may not know to re-render, and any memoization relying on that reference silently breaks. Always create new objects and arrays instead.
Why is todos.push(newTodo); setTodos(todos); a bug-prone pattern in React?
- →The array reference doesn't change, so React may not detect the update
- →push() is too slow to be used in React
Meaningful Names, Not Generic Ones. Name components and props for what they represent, not their implementation. UserCard communicates intent; Component1 or Wrapper2 does not. The same goes for boolean props: isLoading and hasError read clearly at the call site, while a bare flag or status={1} forces the reader to go look up what it means.
Mastery Achieved. You now have a working checklist for professional React code: single-responsibility components, state colocated where it's used, useEffect reserved for real synchronization, strict immutability, and clear, honest naming. Next, you'll build on this with 'Thinking in React' — the process of designing a component tree from a design or requirement.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
These are code-organization practices, not browser-dependent features.
Fully applicable.
Fully applicable.
Fully applicable.
Accessibility (A11y)
1Small, Focused Components Are Easier to Audit for Accessibility
A component with a single, clear responsibility is much easier to check for correct semantics, labeling, and keyboard behavior than a large component mixing several unrelated concerns.
SEO Implications
- 1
Predictable Component Structure Supports Consistent Server Rendering
Well-organized, single-responsibility components are easier to correctly split between Server and Client Components, which directly affects how much indexable HTML a page ships without relying on client JavaScript.
Best Practices
Ask 'Can This Be Derived?' Before Reaching for useEffect
Before syncing a value into state via useEffect, check whether it can just be computed directly during render from existing props or state — this avoids an extra render pass and an extra place for bugs to hide.
Treat All State and Props as Read-Only
Never call array or object mutation methods (push, splice, direct property assignment) on state or props — always produce a new array or object with spread syntax or array methods like map/filter that return new arrays.
Frequent Bugs
A list doesn't visually update even though setTodos was called with new data.
The array was mutated in place (e.g., with push or splice) before being passed to the setter, so its reference didn't change and React's bailout logic skipped the re-render. Always spread into a new array: setTodos([...todos, newTodo]).
A derived value computed in a useEffect is one render behind the state it depends on.
Storing a derived value in its own state updated via useEffect introduces a render lag, since the effect runs after the render that changed its dependencies. Compute the derived value directly during render instead of storing it in separate state.
Real-World Examples
Refactoring a Monolithic Settings Page
A 600-line SettingsPage component handled profile editing, password changes, and notification preferences all in one file with a dozen useState calls. Splitting it into ProfileForm, PasswordForm, and NotificationSettings — each owning its own local state — made each piece independently testable and reduced unrelated re-renders across the page.
function SettingsPage() {
return (
<div>
<ProfileForm />
<PasswordForm />
<NotificationSettings />
</div>
);
}