Lesson 19 of 20

React Best Practices

Components: One Job, Honest Names

A component should do one thing, and its name should say what that thing is. Those two sentences cover most of what people mean by clean React. The failure mode is not a component that is too long in lines; it is a component that has quietly taken on four responsibilities — fetching, filtering, laying out and handling a modal — so that changing any one of them means understanding all four.

A practical test: try describing what the component does in one sentence without using the word and. If you cannot, you have found the split. Line count is a rough proxy for the same thing, which is why the usual advice about two hundred lines works as a warning sign rather than a rule.

Names should describe what a thing is, not what it looks like today. ProductCard survives a redesign; BlueBox does not. Boolean props read best with a verb: isLoading, hasError, canEdit. Handler props start with on and the functions implementing them with handle. None of this is enforced by any tool, and all of it makes a codebase readable by someone who has never seen it, including yourself in six months.

One file per component, named after the component, in a folder with its stylesheet and its tests. Small helper components used only by that file can share it — they do not need a folder of their own until someone else needs them.

Example
// Doing too much: fetching, filtering, layout and a modal in one component
function Dashboard() { /* 300 lines */ }

// Split along the responsibilities
function Dashboard() {
  const { orders, isLoading, error } = useOrders();     // data -> a hook

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;

  return (
    <DashboardLayout>
      <OrderFilters />
      <OrderTable orders={orders} />
    </DashboardLayout>
  );
}

// Naming that survives contact with a redesign
<ProductCard product={p} isFeatured onAddToCart={handleAdd} />
// not
<BlueBox data={p} flag={true} cb={fn} />
Notes
  • The best-structured components tend to read like a description of the screen, with the machinery — data loading, subscriptions, timers — moved into hooks. If you cannot see the shape of the UI in a component's JSX, that is the signal to extract.

State: Four Questions Before You Add Any

More bugs come from badly organised state than from anything else in React, and almost all of them are avoidable by asking four questions before adding a useState.

Does this need to be state at all? If it can be calculated from other state or props — a total, a count, a filtered list, whether the form is valid — calculate it during render. Storing it creates a second copy that can disagree with the first. Is it already stored somewhere else? Keeping both a selectedProduct object and a selectedId means two things to update; keep the id and look the object up.

Where should it live? In the lowest component that contains everyone who needs it. Higher than that and you re-render half the app for a value one component uses. Lower and you cannot share it. Does changing it re-render anything? If not — a timer id, a one-shot flag, a previous value — it belongs in a ref, not in state.

Then two habits that prevent the rest. Update immutably, always, so React can see the change: spread, map and filter, never push or direct assignment. And use the updater form of the setter whenever the next value depends on the previous one, so that a stale closure cannot produce a wrong result.

Example
// Duplicated state: two things to keep in step
const [products, setProducts] = useState([]);
const [selected, setSelected] = useState(null);   // a whole object

// Better: store the id, derive the object
const [selectedId, setSelectedId] = useState(null);
const selected = products.find(p => p.id === selectedId) ?? null;

// Derived, not stored
const total = cart.reduce((s, i) => s + i.price * i.qty, 0);
const isValid = form.email.includes('@') && form.password.length >= 8;

// Immutable updates
setItems([...items, item]);
setItems(items.filter(i => i.id !== id));
setUser({ ...user, city: 'Nagpur' });

// Updater form when the next value depends on the last
setCount(c => c + 1);
  • Can it be calculated? Then calculate it — do not store it
  • Is it already stored elsewhere? Keep one copy, derive the rest
  • Put it in the lowest component that covers everyone who needs it
  • Does the screen depend on it? If not, use a ref
  • Always replace, never mutate
  • Use the updater form when the new value depends on the old one
Notes
  • Two pieces of state that must always be changed together are a design smell. Either one is derived from the other, or they are really one object.

Discipline Around Effects

Effects deserve their own set of rules because they are where correctness quietly slips. The first and most valuable: before writing one, ask which system outside React you are synchronising with. If you cannot name one — the network, a timer, storage, the document, a third-party library — the code almost certainly belongs in a calculation during render or in an event handler.

The second: put everything the effect reads into the dependency array, and never delete an entry to make a warning stop. If the array feels wrong, the effect is wrong. Common fixes are moving a function inside the effect, depending on primitive values rather than an object, or stabilising an object with useMemo.

The third: every subscription, timer and listener gets a cleanup function. That is what makes StrictMode's deliberate double-run in development harmless, and it is what stops a long session accumulating dead listeners. If your effect starts something, the returned function stops it.

The fourth: for anything that fetches, guard against out-of-order responses with an ignore flag or an AbortController. Two clicks in quick succession is not an edge case; it is Tuesday.

Example
// The shape a well-behaved fetching effect has
useEffect(() => {
  let ignore = false;

  loadOrders(userId).then(data => {
    if (!ignore) setOrders(data);
  });

  return () => { ignore = true; };
}, [userId]);

// Belongs in a handler, not an effect
function handleCheckout() {
  trackEvent('checkout_started');
  createOrder(cart);
}

// Belongs in render, not an effect
const itemCount = cart.reduce((n, i) => n + i.qty, 0);

// Belongs in a key, not an effect
<EditForm key={selectedId} record={record} />
Notes
  • Install eslint-plugin-react-hooks on day one of every project. It enforces the rules of hooks and reports missing dependencies, and it catches a whole class of bug before the code ever runs.

A Folder Structure That Survives Growth

Small projects are fine with folders named after what things are: components/, hooks/, utils/. That arrangement starts to hurt at around thirty components, when working on one feature means opening five folders and every folder contains files from unrelated parts of the app.

The alternative is to organise by feature. Everything belonging to authentication — its components, its hook, its context, its API calls — lives in features/auth/. Everything for the cart lives in features/cart/. Genuinely shared building blocks such as Button and Modal stay in a top-level components/, and genuinely shared helpers in lib/.

Two things make this work in practice. Keep files close to where they are used — a component used by exactly one feature belongs inside that feature, not in the shared folder. And promote to shared only when a second feature actually needs it, not when you imagine one might.

There is no official React structure and no need to agonise. The test is whether a new person can find the code for a feature by guessing, and whether deleting a feature means deleting one folder. If both are true, the structure is working.

Example
src/
├── components/            # shared, generic, used by several features
│   ├── Button/
│   │   ├── Button.jsx
│   │   └── Button.module.css
│   └── Modal/
├── features/
│   ├── auth/
│   │   ├── LoginForm.jsx
│   │   ├── AuthContext.jsx
│   │   ├── useAuth.js
│   │   └── authApi.js
│   └── cart/
│       ├── CartDrawer.jsx
│       ├── CartItem.jsx
│       ├── CartContext.jsx
│       └── useCart.js
├── hooks/                 # shared hooks: useDebounce, useLocalStorage
├── lib/                   # api client, formatters, constants
├── pages/                 # one component per route
├── App.jsx
└── main.jsx
Notes
  • Start with the simple structure and move to features when the pain appears. Refactoring folders is a low-risk change — your editor updates the imports — so there is nothing to gain from over-planning it on day one.

The Things Reviewers Actually Find

The list below is what an experienced reviewer looks for first in a student or junior project, and it is worth running over your own code before you show it to anyone. None of these items is difficult; they are simply the things that get forgotten when a feature finally starts working and attention moves on.

Two of them deserve emphasis because they separate a portfolio project from a toy. The first is handling every state a screen can be in — loading, error, empty and populated. A demo that only works when the network is fast and the data is present reads as unfinished to anyone who has shipped software.

The second is accessibility, which costs almost nothing if you do it while writing rather than afterwards. Use a <button> for actions and an <a> for navigation instead of clickable <div>s, label every form field, give images meaningful alt text, and check you can operate the page with the Tab key alone. That is most of the benefit, and it is also better React — the semantic elements come with keyboard and screen-reader behaviour you would otherwise have to rebuild.

  • Every list has a stable key, and it is not the array index
  • Loading, error and empty states exist on every screen that fetches data
  • No secrets, API keys or tokens in the frontend code or in VITE_ variables
  • Forms use onSubmit with preventDefault, and disable the button while saving
  • Buttons are <button>, links are <a>, inputs have labels, images have alt
  • No console errors or warnings in the browser during normal use
  • npm run build succeeds and npm run preview works before deploying
  • No commented-out code, unused imports or leftover console.log calls
  • The README says what the project is and how to run it
Notes
  • A clean browser console is a surprisingly strong signal. React's warnings are specific and actionable, and a project with twenty of them scrolling past tells a reviewer that nobody has been reading them.

Tools Worth Setting Up Once

A small amount of tooling removes whole categories of mistake permanently, and all of it takes a few minutes at the start of a project.

ESLint with the React hooks plugin is the highest-value item on the list — it catches conditional hook calls and missing effect dependencies, which are two of the hardest bugs to diagnose by reading. Prettier ends every argument about formatting by making it automatic. The React Developer Tools extension lets you inspect the component tree, read props and state live, and profile renders; not using it means guessing at things you could simply look at.

TypeScript is worth a serious mention. On a project past a few screens it catches the mistakes that props are most prone to — a misspelled prop name, a number where a string was expected, a possibly-undefined value used without a check — at the moment you type them rather than in the browser. It is a real investment in learning, so do not take it on at the same time as React itself. If you want a fraction of the benefit for none of the cost, defining a clear prop list and using default values gets you part of the way.

Finally, learn to read React's error messages properly. They name the component, describe the rule that was broken, and usually suggest the fix. A large part of becoming productive in React is simply not skipping past them.

  • eslint-plugin-react-hooks — enforces the rules of hooks and dependency arrays
  • Prettier — automatic formatting, configured once and forgotten
  • React Developer Tools — inspect the tree, read state, profile renders
  • TypeScript — worth it on any project you will maintain, once React is comfortable
  • A component library — Material UI, Chakra or shadcn/ui, when the UI is not the point
  • React Router, and a data library such as TanStack Query, once the app is real
Notes
  • The React documentation at react.dev is unusually good and is kept current with the version you are actually using. Prefer it to blog posts, which are frequently written for older versions and are the main source of outdated advice you will encounter.
Ask AI