DEV Community

A0mineTV
A0mineTV

Posted on

React Reactivity Explained: State UI (with a practical example)

In React, “reactivity” is not magic. It’s a very simple mental model:

Your UI is a projection of your state.

You don’t manually “refresh the page” or imperatively manipulate the DOM to reflect changes.
Instead, you update state, and React re-renders the parts of the UI that need to change.

This post breaks that down in a practical way, using a small (but real-world) example.


The core idea: data drives the interface

In traditional DOM-driven code, you often end up doing things like:

  • selecting elements,
  • adding/removing nodes,
  • toggling classes,
  • keeping UI and data in sync manually.

In React, you flip that:

  • State is the source of truth
  • Rendering describes what the UI should look like for a given state
  • Events update state
  • React handles the UI updates

Once this clicks, building UIs becomes much more predictable.


Why this feels great in real projects

This approach usually improves:

  • Maintainability: fewer hidden side effects and “who changed the DOM?” bugs
  • Readability: UI logic is expressed through state changes
  • Consistency: the UI stays aligned with your data
  • Scalability: you can grow features without turning the codebase into spaghetti

Practical example: search + filtered list + toggle

Let’s build a small “Tasks” UI that demonstrates the full loop:

  • A query state for the search input
  • A tasks state as our data source
  • A derived list visibleTasks based on query + tasks
  • A toggle(id) action that updates the state (and the UI follows automatically)

The code

import { useMemo, useState } from "react";

export default function App() {
  const [query, setQuery] = useState("");
  const [tasks, setTasks] = useState([
    { id: 1, title: "Fix login bug", done: false },
    { id: 2, title: "Write API docs", done: true },
    { id: 3, title: "Refactor dashboard", done: false },
  ]);

  // Derived data: computed from state (not stored as separate state)
  const visibleTasks = useMemo(() => {
    const q = query.trim().toLowerCase();
    if (!q) return tasks;
    return tasks.filter((t) => t.title.toLowerCase().includes(q));
  }, [tasks, query]);

  const toggle = (id) => {
    setTasks((prev) =>
      prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
    );
  };

  return (
    <div>
      <h1>Tasks</h1>

      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search a task..."
      />

      <ul>
        {visibleTasks.map((t) => (
          <li key={t.id}>
            <label style={{ textDecoration: t.done ? "line-through" : "none" }}>
              <input
                type="checkbox"
                checked={t.done}
                onChange={() => toggle(t.id)}
              />
              {t.title}
            </label>
          </li>
        ))}
      </ul>

      <p>
        Showing <strong>{visibleTasks.length}</strong> / {tasks.length}
      </p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

What’s happening here (step by step)

1) State is the source of truth

We store two pieces of state:

  • tasks: the actual task data
  • query: what the user typed in the search input
const [query, setQuery] = useState("");
const [tasks, setTasks] = useState([...]);
Enter fullscreen mode Exit fullscreen mode

Nothing else “owns” the UI. If the UI changes, it’s because one of these changed.


2) The UI describes a view of the state

This is the React mindset:

  • for each task in visibleTasks, render a <li>
  • checkbox state comes from t.done
  • the label style depends on t.done
{visibleTasks.map((t) => (
  <li key={t.id}>
    <label style={{ textDecoration: t.done ? "line-through" : "none" }}>
      <input
        type="checkbox"
        checked={t.done}
        onChange={() => toggle(t.id)}
      />
      {t.title}
    </label>
  </li>
))}
Enter fullscreen mode Exit fullscreen mode

You’re not “editing HTML”.
You’re declaring what should be displayed for the current state.


3) Updates are done via state setters (not direct mutation)

This part is crucial.

When the user toggles a checkbox, we call:

setTasks((prev) =>
  prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
Enter fullscreen mode Exit fullscreen mode

We are not mutating tasks in place.
We create a new array and a new object for the updated task.

That immutability is what makes updates predictable and easy to reason about.


4) Derived data: compute it, don’t store it

A common mistake is to store the filtered list as another state value.

Instead, compute it from the source state:

const visibleTasks = useMemo(() => {
  const q = query.trim().toLowerCase();
  if (!q) return tasks;
  return tasks.filter((t) => t.title.toLowerCase().includes(q));
}, [tasks, query]);
Enter fullscreen mode Exit fullscreen mode

Why ?

  • It reduces bugs (no duplicated sources of truth)
  • It keeps your app consistent (one real state)
  • It makes refactors easier

Note: For small lists you can skip useMemo and just compute inline.
I’m using it here to highlight the “derived data” concept.


The mental model to remember

When you write React, think in this loop:

  1. State holds your data
  2. Render is a function of that data
  3. Events trigger state updates
  4. React updates the UI

That’s “reactivity” in React.


Where this scales (real apps)

This same pattern powers:

  • dashboards (filters, sorting, pagination)
  • forms (validation state, errors, submission states)
  • complex UIs (tabs, modals, multi-step flows)
  • data-heavy components (search, selection, bulk actions)

As your UI grows, you can move to:

  • useReducer for more complex state transitions
  • context or state libraries when the state becomes global
  • server-state tools (React Query, SWR) for async data

But the core idea remains the same: state → UI.

Top comments (0)