Use Cases (Updated: 6/7/2026)

A Decision Map for React Basics: Where State Lives, How to Draw Component Boundaries, When to Use Hooks

Where state lives, when to use useEffect, where to split components: a React design decision map with runnable code and Claude Code prompts.

A Decision Map for React Basics: Where State Lives, How to Draw Component Boundaries, When to Use Hooks

“Where am I actually supposed to put this in React?”

That is the question I get asked more than any other. The tutorial is finished. You know the names useState and useEffect. And yet, the moment you start building a real screen, your hands freeze. Does this state belong inside this component, or do I lift it up to the parent? Do I fetch it in a useEffect, or not? Where do I draw the line and split this into another component?

After enough agonizing, you give up and cram everything into one giant component. It works. But three days later, you cannot read your own code. I was right there too.

Most of the time, the reason you get stuck in React is not syntax. It is that you have no decision axis to design against. This article turns that decision axis into a single map. The fine-grained details (the deeper hook mechanics, how to pick a state management library, how to build tables and forms) get handed off to linked articles along the way, so here you can just grab the whole picture first.

Key takeaways

  • React design is mostly two decisions: where state lives and where component boundaries fall. Get those two right and 90% of the mess disappears.
  • useEffect is for syncing with the outside world. If a value can be computed from props and state, it does not need an Effect.
  • Re-rendering is not the enemy. Add useMemo or memo only when you actually feel it is slow — never preemptively.
  • When you ask Claude Code for help, hand it four things before “build it”: boundaries, state location, data shape, and how to verify. The output gets dramatically more stable.
  • Libraries (Zustand, TanStack Query, React Hook Form) are safest when you first express the intent in plain React, then reach for them.

React, in one sentence

React is just a machine that says: when state changes, redraw the screen to match that state. That is the whole idea.

This is the part that differs most from older approaches. With jQuery, you gave imperative commands by hand: “when this button is clicked, rewrite the text of this element.” React flips it around. You describe the result — “given the current state, the screen should look like this” — and when state changes, React computes the diff and redraws only the parts that need it.

So the main character of your design is not the markup (the HTML). It is state. “What values does this screen need to remember?” and “where do I put them?” come first. That is the starting point of React development, and if you get it wrong, everything downstream becomes painful.

90% of design: where to put state

State has roughly four homes. The trick is to consider them top to bottom and only move down when the current level is not enough.

HomeGood forExamples
Nowhere (derive it)Values you can compute from props or stateFiltered array, total price, display label
Inside one component (local)Values only that component usesOpen/closed flag, in-progress input text
Lifted to the parentValues shared between sibling componentsThe active tab, the filter condition
URL / server sideState you want to share or reproduceSearch query, page number, API data

The most common mistake is skipping the first row. People dutifully put “the filtered user list” into a useState and then recompute it inside a useEffect. You do not need that. If you have the original array and the filter condition, just compute it on every render. Every extra piece of state adds another place where you have to worry whether things are “in sync” — and that is exactly where bugs breed.

Conversely, if siblings need to see the same value, lift it to a common parent without hesitation. This is precisely the thinking React’s own docs lay out in Thinking in React: find the minimal representation of state, then place it at the lowest position where every component that needs it can share it. That is all.

Once state grows complex and spans multiple screens, libraries earn their keep. But jumping straight to “Redux, I guess” makes things heavy. I split the decision axis — how to choose by scale and kind — into choosing a state management approach, so read that once useState starts to hurt.

Where to split components

The other axis is boundaries. The rule of thumb is simple: can you explain its props in one sentence?

Take UserStatusBadge — “it takes a status and shows a colored label.” That is a good component. By contrast, if your UserTable has the search logic, the API call, the edit-modal open/close, and the save handler all stuffed inside it, you can no longer explain it in one sentence. That is the sign it is time to split.

When you do split, dividing by role keeps things clear:

  • Presentational components (UI): receive props and just render. They hold no state.
  • Components with state and logic (containers): fetch data and distribute it to children.
  • Reusable logic (custom hooks): push state and side effects out into a function.

Watch out: over-splitting is a failure too. If you componentize line by line into UserNameText, UserEmailText, UserRoleText, you only multiply the file count while organizing no responsibility at all. Splitting is worth it only when it helps one of three things: reuse, separating state, or making something easier to test. Make “do not generalize until the second use site” your mantra and you will dodge a lot of over-engineering.

The right home for useEffect

Here I will line up just enough of the hook story to keep within the boundaries of this map.

useState is “the value the screen remembers.” useEffect is for syncing with the world outside React. Once that distinction clicks, 90% of hooks stop being confusing.

“The outside world” means API calls, localStorage, timers, subscribing to browser events — anything that touches the outside beyond drawing the screen. Put the other way: a value you can compute from props and state does not need a useEffect. This is the single most common React mistake, so much so that the official docs dedicate a whole page to it: You Might Not Need an Effect.

When in doubt, walk this order:

  1. Can it be computed from props or state? → compute it inline during render (no Effect).
  2. Is it triggered by a user action? → put it in an event handler (fetch on click, etc.).
  3. Does the rendered output itself need to stay in sync with something external? → only now reach for useEffect.

Case 3 covers things like “while this screen is open, fetch and display data from the server.” The custom hook below is exactly that case made concrete.

Copy-paste runnable: push fetch logic into a custom hook

A map alone gets dull, so here is one piece you can actually run. It is a data-fetching custom hook that works as-is in Vite + React + TypeScript. It shows the “right place” for useEffect and how to write the cleanup, both at once.

Two points matter. Cancel stale requests with AbortController. And do not touch state after unmount. Forget those two and you get a quiet bug where, when you switch screens fast, an old result overwrites the new screen.

import { useEffect, useState } from "react";

// Represent fetch status as a single value, so loading / success / error never get mixed up
type FetchState<T> =
  | { status: "loading"; data: null; error: null }
  | { status: "success"; data: T; error: null }
  | { status: "error"; data: null; error: string };

export function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    status: "loading",
    data: null,
    error: null,
  });

  useEffect(() => {
    // The "cancel button" that aborts an in-flight request
    const controller = new AbortController();
    setState({ status: "loading", data: null, error: null });

    async function run() {
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const json = (await res.json()) as T;
        setState({ status: "success", data: json, error: null });
      } catch (err) {
        // An abort is not a "failure", so just bail out quietly
        if (err instanceof DOMException && err.name === "AbortError") return;
        setState({
          status: "error",
          data: null,
          error: err instanceof Error ? err.message : "Unknown error",
        });
      }
    }

    void run();

    // Before url changes, or before the screen unmounts, stop the running request
    return () => controller.abort();
  }, [url]);

  return state;
}

The calling side looks like this. Because you branch on status, you can never construct a contradictory state like “data exists but it is still loading.”

type User = { id: string; name: string };

export function UserList() {
  const result = useFetch<User[]>("/api/users");

  if (result.status === "loading") return <p>Loading…</p>;
  if (result.status === "error") return <p role="alert">Failed: {result.error}</p>;

  return (
    <ul>
      {result.data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

This is the template for “the right way to use an Effect.” In real work you will start wanting caching and refetching, and at that point you switch over to the cache design of TanStack Query. Writing it yourself is only to get the feel of “separating UI from communication” into your bones.

Re-rendering is not scary

You hear a lot that “React re-renders too much and is slow,” but let me say upfront: a re-render itself is, in most cases, practically free. React only commits the diff to the DOM, so a function re-running does not make the screen flicker.

And yet, preemptively plastering useMemo and React.memo everywhere just makes the code harder to read and usually does not speed anything up. The order is backwards. The correct order is this:

  1. First, put state where it belongs (do not drag unrelated components into the re-render).
  2. Give list keys a stable ID (do not use the index as the key).
  3. Wrap heavy computation or large-array processing in useMemo only after you actually feel it is slow.
  4. If heavy work runs while typing, add debouncing (thinning out the calls).

Adding memo without being able to explain why a re-render is happening is like slapping a bandage on a spot that does not hurt. Measure, pinpoint the bottleneck, then fix it. I wrote up that way of thinking in detail in prioritizing web performance work.

Building it efficiently with Claude Code

The knack here is to hand Claude Code the decision axes above, rather than dumping the whole job on it. “Build me a nice admin screen” gets you something that looks tidy but has state scattered all over. That is exactly what I did wrong once, and I ended up rewriting almost the entire giant component it produced.

These days, before I ask, I always hand over these four things in plain prose:

Information to hand overExample phrasing
Component responsibilities”Split the list, the search, and the edit dialog into separate components.”
Where state lives”Keep the filter condition in the parent; only the row’s open/closed goes local.”
Data shape”Here is the User type. Do not change the shape of the API response.”
How to verify”Also write a test that queries by role and label with Testing Library.”

And the thing that helped me most personally: make it a “review request” rather than a “create from scratch.” Have it read the existing diff and call out “is any component too big?”, “are there unnecessary useEffects?”, “is UI state mixed with server data?” It is much less likely to spawn new files on its own, and the change stays small.

If you work on a team, write these four points and the prohibitions (excessive generalization, placeholder-only forms, using the index as a key, etc.) into CLAUDE.md so you do not repeat the same explanation every time. The typing foundation continues seamlessly in practical TypeScript tips, and how to widen test coverage in prioritizing your test strategy.

FAQ

Q. How do I choose between useState and useRef? A. Values you want reflected on screen go in useState; values you do not need reflected go in useRef. For example, “a DOM reference to an input field” or “holding the previous value” need no re-render, so useRef. If you want the screen to change when the value changes, useState. Changing a useRef value does not trigger a re-render.

Q. How many props before I should split a component? A. Judge by “can I explain it?”, not by count. Three props can be too big if the roles are scattered, and six are fine if they all serve the same purpose. When props pile up, first suspect “could this collapse into one object?” and “are two components actually mixed together here?”

Q. Data I fetched inside a useEffect briefly looks stale when I switch screens. A. The cleanup is missing. As in the code example above, stop the previous request with AbortController and always write return () => controller.abort(). That removes the race where “an old result overwrites the new screen.”

Q. Should I add a state management library from the start? A. No. Go with useState and “lift it to the parent” first, and consider a library only once state gets tangled across screens. Server data is a different thing from UI state to begin with, so separate the kinds first in choosing a state management approach before you pick one.

Q. Claude Code keeps building overly grand, general-purpose components. A. Tell it upfront: “only the abstraction we need right now” and “do not generalize until the second use site.” Get a generic Table, generic Modal, and generic FilterEngine built for a single screen, and future changes suddenly get heavy.

Wrapping up

React design, boiled all the way down, is just answering two questions: where does this value live? and can I state this component’s responsibility in one sentence? Consider state homes top to bottom, do not use an Effect for values you can derive, and touch re-rendering only after it feels slow. That alone gets you code you can actually read.

Claude Code becomes a reliable partner the moment you hand it those decision axes. Dump the axes along with the job and you get code that looks impressive but is weak in operation. Start small with a single screen, handing over the four points: boundaries, state, data shape, and how to verify.

When you want to dig deeper into the specifics, go to choosing a state management approach for state, the cache design of TanStack Query for data fetching, react-hook-form with zod validation for forms, and prioritizing a11y work for accessibility. If you want hands-on material or someone to ask, take a look at the product lineup too.

#Claude Code #React #components #Hooks #useEffect
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.