Get eBook BundleVolume I index
State and Events From Scratch

Derived UI from State

Ishtmeet Singh @ishtms/July 20, 2026/13 min read
#react#state#derived-data#rendering#forms

One mistake you'll see quite often in React code (atleast I have) is people storing way too much stuff in state.

You have a search query, so obviously that goes in state. Fine. Then you filter some lessons using that query, and now that filtered array also goes in state. Then maybe you store the count too. Then a label based on the count. And after some time you're sitting there with six state variables even though the user can actually change only one or two things.

React doesn't need state for every value that changes on screen.

State is mostly for values which can change independently because something happened. User typed something, clicked something, server request finished, form got submitted, stuff like that. If some other value can be calculated from the state and props you already have, just calculate it while rendering.

Take this:

jsx
function LessonSearch({ lessons }) {
  const [query, setQuery] = useState("");

  const normalizedQuery = query.trim().toLowerCase();
  const visibleLessons = lessons.filter((lesson) =>
    lesson.title.toLowerCase().includes(normalizedQuery),
  );

  return <LessonList lessons={visibleLessons} />;
}

What can the user actually change here?

query.

So query is state.

Can the user independently change normalizedQuery? Nope. It's whatever query.trim().toLowerCase() gives us. Same for visibleLessons. That array depends on lessons and query, so we can just calculate it every time the component renders.

No setter needed.

Duplicate state gives you extra work

Let's make the same example worse.

jsx
const [query, setQuery] = useState("");
const [visibleLessons, setVisibleLessons] = useState(lessons);

Now we are storing the search text and also storing the result of filtering with that search text.

So every time query changes, we need to remember to update both:

jsx
function handleQueryChange(event) {
  const nextQuery = event.target.value;
  setQuery(nextQuery);
  setVisibleLessons(filterLessons(lessons, nextQuery));
}

Looks okay, right?

Well, what happens if lessons changes from the parent?

Nothing happened to the search input, so handleQueryChange() never ran. Your visibleLessons state can now contain results from the old lessons array while the component has already received a newer one.

Now you have two values which are supposed to agree with each other, and React isn't going to magically keep them synced for you.

Much easier:

jsx
const visibleLessons = filterLessons(lessons, query);

That's it.

Every render uses the current lessons and current query, so the result can't be hanging around from some older combination.

Counts usually don't need state either

Suppose you've already got:

jsx
const visibleLessons = filterLessons(lessons, query);

And now you want to show how many are visible.

jsx
const visibleCount = visibleLessons.length;

Done.

jsx
return (
  <section>
    <p>{visibleCount} lessons shown</p>
    <LessonList lessons={visibleLessons} />
  </section>
);

There's no reason for:

jsx
const [visibleCount, setVisibleCount] = useState(0);

because what event changes visibleCount independently?

None.

It changes because visibleLessons changed. So calculate it from visibleLessons.

Also make sure you're counting the same data you're actually displaying. If the screen shows the filtered array but you do this:

jsx
const visibleCount = lessons.length;

then maybe two lessons are visible while your text proudly says "6 lessons shown".

Very helpful.

If two parts of the screen describe the same result, derive both from the same data.

One boolean can produce multiple bits of UI

Let's say you've got some details section:

jsx
const [isOpen, setIsOpen] = useState(false);

The button text can come from that:

jsx
const buttonLabel = isOpen ? "Hide details" : "Show details";

And so can the accessibility state:

jsx
<button aria-expanded={isOpen}>
  {buttonLabel}
</button>

We store one fact: is the thing open?

Then we calculate how that fact should appear in different places.

You could store the label separately:

jsx
const [buttonLabel, setButtonLabel] = useState("Show details");

but now every place which changes isOpen also needs to remember changing buttonLabel.

Sooner or later somebody updates one and forgets the other. Now the panel is open and the button says "Show details".

Nice.

Store the actual fact once. Calculate the text and props from it.

Form validity is normally calculated from the input

Number inputs are a good example because people sometimes try converting them to numbers too early.

Suppose you're asking for a weekly lesson goal:

jsx
const [weeklyGoal, setWeeklyGoal] = useState("3");

I'm keeping it as a string here because that's what the input is editing. An empty input is also a completely normal state while the user is typing.

Then during render:

jsx
const numericGoal = Number(weeklyGoal);

const hasValidGoal =
  weeklyGoal !== "" &&
  Number.isInteger(numericGoal) &&
  numericGoal >= 1 &&
  numericGoal <= 30;

And now the UI can use that:

jsx
<p>
  {hasValidGoal
    ? "Goal accepted"
    : "Enter a whole number from 1 to 30"}
</p>

What did the user change?

Only weeklyGoal.

numericGoal is calculated from it. hasValidGoal is calculated from that too. So neither needs its own state.

Now, submission status is different.

If we send this value to a server, maybe we need isSaving, saveError, or something similar. Those values don't come from the text inside the input, so yeah, those can have their own state.

Valid input and "show the error now" are different

Another easy one to mix together.

Suppose the input is invalid as soon as the page loads. Do you want a red error screaming at the user before they've even touched anything?

Probably not.

So validity can be calculated:

jsx
const hasValidGoal = /* calculation */;

while "has the user tried submitting?" can be state:

jsx
const [submitted, setSubmitted] = useState(false);

Then:

jsx
const showGoalError = submitted && !hasValidGoal;

Those values change for different reasons.

hasValidGoal changes when the input changes.

submitted changes because the user attempted submission.

jsx
function handleSubmit(event) {
  event.preventDefault();
  setSubmitted(true);

  if (!hasValidGoal) return;

  saveGoal();
}

So don't try to work out whether the form was submitted by looking at the input text. Those are two different facts.

Store the selected ID, not a copied object

Suppose the parent gives us this:

jsx
[
  { id: 1, title: "State" },
  { id: 2, title: "Effects" }
]

and the user selects one lesson.

Usually, store the ID:

jsx
const [selectedId, setSelectedId] = useState(lessons[0]?.id ?? null);

Then find the current object during render:

jsx
const selectedLesson =
  lessons.find((lesson) => lesson.id === selectedId) ?? null;

Why not store both?

jsx
const [selectedId, setSelectedId] = useState(first.id);
const [selectedLesson, setSelectedLesson] = useState(first);

Because now you've copied the same selection into two state values.

Imagine the parent sends a newer lessons array where the selected lesson has been renamed. Your stored selectedLesson object can still contain the old title, while the list itself has the new title.

The ID doesn't have that issue. It only says which lesson is selected. Then you fetch that lesson from the current array you're rendering right now.

What if the selected item disappears?

Good question.

This can happen:

jsx
const selectedLesson = lessons.find(
  (lesson) => lesson.id === selectedId,
);

If the lesson got removed, find() gives you undefined.

That's not some weird React problem. Your selection just points at something which no longer exists.

So render a sensible result:

jsx
if (!selectedLesson) {
  return <p>Select an available lesson.</p>;
}

If the deletion happened through one of your own event handlers, you can clear the selection there:

jsx
function handleDelete(lessonId) {
  setLessons((current) =>
    current.filter((lesson) => lesson.id !== lessonId),
  );

  setSelectedId((current) =>
    current === lessonId ? null : current,
  );
}

The handler already knows which lesson is being removed, so that's a perfectly reasonable place to update related independent state.

What I would not do is see the failed lookup during render and immediately call setSelectedId() there. Now you're changing state while React is in the middle of rendering, which is not what you want.

Render the empty case. Fix local state in the event which caused the change.

Too many booleans can create impossible combinations

Suppose some request has these:

jsx
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [isComplete, setIsComplete] = useState(false);

Can all three become true?

Yep.

Does "loading, failed, and completed at the same time" make any sense for this request?

Probably not.

If these values are mutually exclusive, store one status:

jsx
const [status, setStatus] = useState("idle");

Maybe it can be:

txt
idle
loading
error
complete

Then if some component wants booleans:

jsx
const isLoading = status === "loading";
const hasError = status === "error";

Those are just derived values.

But don't take this too far either. If a lesson can be complete and featured at the same time, those are independent facts. They don't belong in one exclusive status field.

Your state should be able to represent the situations your app actually supports, without also allowing a bunch of nonsense combinations.

Don't copy props into state just because you can

This looks innocent:

jsx
function LessonTitle({ title }) {
  const [localTitle, setLocalTitle] = useState(title);

  return <h2>{localTitle}</h2>;
}

But useState(title) only uses title when that state is first created.

If the parent later sends:

jsx
title="React Effects"

your localTitle doesn't automatically become "React Effects".

If the component is only displaying the title, just use the prop:

jsx
function LessonTitle({ title }) {
  return <h2>{title}</h2>;
}

Now sometimes you actually do want local state from an initial prop.

Maybe this is an editor:

jsx
function TitleEditor({ initialTitle }) {
  const [draft, setDraft] = useState(initialTitle);

  // ...
}

That's fine because now we're saying something different: "initialTitle gives me the starting value, and after that this component owns an editable draft."

Even the name initialTitle helps communicate that.

But now you'll need some policy for what should happen if the user switches to another record while editing. Maybe changing the component key starts a fresh editor instance. Maybe you explicitly reset the draft. Depends on your UI.

The important bit is that this is an editing workflow, not just blindly mirroring a prop into state.

You don't need an Effect for normal calculations

This one pops up a lot:

jsx
const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Why?

We already have firstName.

We already have lastName.

Just do:

jsx
const fullName = `${firstName} ${lastName}`;

The Effect version actually makes React do more work. First the component renders with whatever old fullName state it currently has. Then React commits. Then the Effect runs and calls setFullName(). Then React renders again.

All that for string concatenation.

Effects are for synchronizing React with stuff outside this render calculation. Browser APIs, network connections, subscriptions, timers, widgets, things of that sort.

Combining two strings isn't one of them.

Shared state should normally live in the shared parent

Suppose you have two sibling components.

One shows a list of lessons.

The other shows details for the selected lesson.

Both need the same selectedId.

Put it in the parent which owns both:

jsx
function ChapterPage({ lessons }) {
  const [selectedId, setSelectedId] = useState(null);

  return (
    <>
      <LessonList onSelect={setSelectedId} />
      <LessonDetails selectedId={selectedId} />
    </>
  );
}

LessonList can update the selection.

LessonDetails can read it.

There's one stored selection.

What you don't want is one selectedId inside LessonList, another one inside LessonDetails, and then some Effect trying to keep both synced. You're making React solve a problem we created ourselves.

For bigger trees you might use context or a state library so the value is available further down. Same idea though: store the independent value once, then let the consumers read it from there.

Calculate first. Optimize when there's actually a reason

Let's say you've got 20 lessons:

jsx
const visibleLessons = lessons.filter(matchesQuery);

Do you immediately need:

jsx
const visibleLessons = useMemo(
  () => lessons.filter(matchesQuery),
  [lessons, query],
);

Probably not.

A filter over a small array is usually cheap enough that the plain version is easier to read and easier to reason about.

useMemo has its own dependency list and cached result. Use it when you've actually got expensive work, or when some reference identity requirement gives you a reason for it.

Also, React Compiler can memoize supported pure component work automatically in projects where you've configured it.

But none of that changes the main rule here. Memoizing duplicate state doesn't make duplicate state correct. You can very efficiently cache the wrong value too.

A quick test before adding state

When you're about to write another useState(), ask what kind of value you're dealing with.

If the value never changes, maybe it's just a constant.

If the parent gives it to you, it's a prop.

If you can calculate it from current props or state, calculate it.

If some event can change it independently, then state probably makes sense.

If the browser itself owns it, such as an uncontrolled form input, maybe you don't need React state for it.

And if the value comes from a server cache or some external store, then that system may own it instead.

These categories aren't permanent either.

A hard-coded list today might become server data next week. Plain text might later become an editable draft. When the behavior changes, then you change where the value lives.

No need to prepare five state variables in advance for a feature which doesn't exist yet.

Build the lesson planner with the minimum state

Suppose our lesson planner lets the user type a search query, hide completed lessons, and enter a weekly goal.

These three values can change independently:

jsx
const [query, setQuery] = useState("");
const [showCompleted, setShowCompleted] = useState(true);
const [weeklyGoal, setWeeklyGoal] = useState("3");

Now everything else can come from those values and the lessons we already have.

jsx
const normalizedQuery = query.trim().toLowerCase();

const visibleLessons = lessons.filter((lesson) => {
  const matchesText =
    lesson.title.toLowerCase().includes(normalizedQuery);

  const matchesStatus =
    showCompleted || !lesson.complete;

  return matchesText && matchesStatus;
});

The goal can be calculated too:

jsx
const numericGoal = Number(weeklyGoal);
const hasValidGoal =
  Number.isInteger(numericGoal) && numericGoal > 0;

And now your visible count is:

jsx
const visibleCount = visibleLessons.length;

Your empty-state message can check visibleLessons.length.

Your button can check hasValidGoal.

Your summary can use numericGoal.

No setters for any of those because the user isn't independently changing any of them.

They're results.

So when query changes, React renders again and recalculates the normalized query, filtered lessons, count, and whatever labels depend on them.

When showCompleted changes, same thing.

When weeklyGoal changes, the numeric conversion and validation run again.

That's the idea I want you to leave with: don't ask "does this value change?" and immediately put it in state.

Ask a slightly better question.

Can this value change on its own, or is it just the result of other values I already have?

If it's the second one, calculate it during render and move on.