Home / Articles / Building a Mental Model for React: Reconciliation, State, and Hooks

This article is published in English.

Building a Mental Model for React: Reconciliation, State, and Hooks

Learn the reasoning behind React's core concepts—reconciliation, components, props, state, and hooks—to build intuition instead of memorizing APIs.

2998 words

If you can already write code but haven't really dug into React—or you've only poked at it briefly—this piece is written with you in mind.

When you start learning React, it's tempting to dive straight into useState, useEffect, props, hooks, and a long list of other APIs. You can pick up the syntax, ship something that runs, and still not really grasp why React behaves the way it does.

This article aims to fix that gap.

Rather than treating React as a pile of APIs to memorize, we're going to explore the reasoning behind React's design and how the pieces fit together. Syntax matters, but it becomes far easier to absorb once you understand what's happening beneath it. So instead of starting with how to write React code, we'll first build up how to reason about React.

We'll cover components, props, state, rendering, reconciliation, hooks, prop drilling, Context, and routing, tying each idea back to the problem it was created to solve. The aim isn't to catalog every API React offers, but to hand you a mental model that makes those APIs click once you meet them.

You've probably heard that some clever algorithm is part of the reason React feels so fast. It can sound almost like magic—how does a JavaScript library manage to update the UI this efficiently?

That algorithm is called reconciliation, and it's a good place to begin.

Reconciliation — An algorithm that made React possible

Diffing two DOM trees from scratch and computing the minimal set of changes needed is a problem that can take roughly O(n³) time to solve.

But suppose the reconciliation process makes a handful of sensible assumptions, and you feed it a few hints about where the changes are likely to be?

Just like that! The complexity drops to roughly O(n).

That means an approach that would naively take about 31 years to finish gets reduced to something closer to 16 minutes—purely by leaning on assumptions and a bit of guidance from the developer.

Hold on… hints? What exactly are these hints, and how do we supply them?

No need to stress. It's simpler than it sounds, and we'll unpack it soon.

For now, let's let reconciliation do its thing behind the scenes and turn to the question that actually matters to us as developers.

But why bother with React at all?

Why React ?

Regardless of what anyone tells you, there's a real mental adjustment when you move from plain HTML, CSS, and JavaScript files toward ideas labeled components, hooks, props, and state.

Next to something like FastAPI or Go, React can genuinely feel like it demands a steeper climb.

But here's the thing: that difficulty is front-loaded.

Once you start pulling back the curtain on React, you'll notice that a lot of these intimidating-sounding ideas rest on surprisingly straightforward foundations.

You're not required to hold your whole application in your head at once.

Instead, you can zero in on a single component—what data it takes in, what it needs to keep track of, and how its rendered output should update.

This compartmentalized way of thinking is what makes building and maintaining complicated interfaces manageable. And at the end of the day, that's the outcome developers actually care about.

Writing code that's easier to build, understand, change, and maintain. The goal here is to help you get there.

The Four Pillars of React :—

Components

A component is, at its heart, a JavaScript function that takes in a single object and hands back some UI written in JSX.

JSX is a syntax extension for JavaScript that lets you embed HTML-like markup right inside your JavaScript code. You can style things using plain CSS or a utility library like Tailwind CSS.

Any sufficiently complex React app is, in the end, just a network of components passing data between each other.

Say you've never touched React before—you can still follow what a component is doing.

const data = {
  question: "What is React?",
  answer: "A JavaScript library for building user interfaces"
};
function FlashCard(data) {
  return (
   <div className="border rounded-lg bg-gray-50 p-4">
       <h2>{data.question}</h2>
       <p>{data.answer}</p>
    </div>
   );
}

Ignoring a couple of React-specific syntax quirks, that's essentially the whole idea of React right there.

Not so bad, right?

Fundamentally, we're feeding some data into a function and getting back a piece of UI.

So as a developer, your real job is designing clean components while keeping three questions in mind:

  1. What information does it take in?
  2. What information does it hold onto?
  3. How should its appearance update?

What could possibly be complicated about a few parameters and some variables? Couldn't we just pass in whatever data we need and store whatever we want in local variables?

Sort of—but not quite.

And what do we mean by UI updating? Picture two salespeople trying to sell the same car, but the manager only tells one of them about a price change.

What happens to the other salesperson? They keep quoting the old price, none the wiser.

Now picture the manager posting the update in a group chat everyone's part of. Change the price once, and the whole team sees it instantly.

That's essentially the problem React was built to solve.

Whenever some piece of information that affects what's shown on screen changes, everything that depends on it—people in our analogy, components in React—needs a dependable way to find out and react to that change. That mechanism is what React provides.

Props

Are function User(name, age, city) and function User(name, city) interchangeable? What about function User(city, name)?

A typical function that relies on positional parameters can only accept a set number of arguments in a specific order—and remember, a component is just a function.

Now imagine you built a component that displays a user's name and age, and it's currently used in 67 different places across your codebase. Then your manager asks you to also show the user's city. Updating every single usage would eat up hours of work.

But what if the function was designed so old calls kept working untouched, while new calls could optionally pass along extra data?

The tool you need here is a single object standing in for a parameter list. React calls this props.

Props are simply how React bundles up all the data handed to a component into one object.

/** So a component like this one **/
function User({ name = "Guest", age = 18, city = "Unknown" }) {
    return (
        <div>
            <h2>{name}</h2>
            <p>{age} years old</p>
            <p>{city}</p>
        </div>
    );
}
/** Can be used in ways like **/
<User />                                    /** Guest, 18, Unknown **/
<User name="Pritam" />                      /** Pritam, 18, Unknown **/
<User name="Rahul" age={21} />              /** Rahul, 21, Unknown **/
<User name="Priya" age={22} city="Delhi" /> /** Priya, 22, Delhi **/

States

Should the dealership manager have kept the new price to himself? Told just one salesperson? Both of them? Or announced it to everyone at the dealership, security guards and cleaning staff included?

Imagine a storage box and you fill it with your belongings. Just by glancing at the box from outside, can you tell if something inside has changed? What if the box gets moved to a different shelf? At the very least, you'd notice that its position shifted and infer something happened.

Now picture that box as the mechanism React uses to hold the values you define.

This means there needs to be a way to push updated values out to whoever depends on them, and a way for those dependents to know when their value has changed.

const [count, setCount] = useState(initialCount);

Have you come across this useState call before?

It hands the component two things:

  • count — the current state value.
  • setCount — a function you call to ask for that state value to be updated.

So what's wrong with just writing value = 5?

React has no way of knowing that something inside the box changed!

Put differently, you need a mechanism to tell React, "Hey, I've updated this value, you might want to refresh the UI."

Is this the hint from earlier? Sort of, yes and no.

useState gives you the ability to store a value, request updates to it, and simultaneously notify React that the change happened. But why does React need to be told explicitly?

Because otherwise you'd end up behaving like that careless car dealership manager.

Remember the actual mistake? The manager changed the price, updated his own copy of the board, and forgot to tell anyone else. You're smarter than that — you'll just call setCount().

So what actually happens once you call setCount()?

React takes the new state, re-renders the component to figure out what the UI should look like now, and then relies on reconciliation to work out what truly needs to change on screen.

You've simply told React that something changed. Reconciliation is what determines what changed and what needs updating as a result.

Hooks

useState() — a built-in function that lets a component hold onto a piece of state and gives you a way to request updates to it.

When that state gets updated, two things can happen:

  • The new value has no effect on what's rendered — React might still re-render, but nothing visibly changes.
  • The new value does affect the UI — React re-renders the component, compares the new output against the previous one through reconciliation, and applies only the necessary DOM changes.

Functions like this one, which carry special React capabilities, are called hooks. A few more are worth knowing.

useRef() — useful when you need a container for a value that has nothing to do with the UI at all.

const count = useRef(0);
count.current++;

So the rule of thumb is: if the value changing should also change the UI, reach for useState(). If only the value itself needs to change, without any rendering consequence, reach for useRef().

It also has a second common use — referencing actual DOM elements — but for now this mental model is enough.

useEffect() — for when you need something to run after React has finished rendering the UI.

useEffect(() => {
    console.log("Runs after every render");
});
useEffect(() => {
    console.log("Runs once after the initial render");
}, []);
useEffect(() => {
    console.log("After the initial render and whenever count changes");
}, [count]); /** Dependencies go here **/

useContext() — consider a component tree where a piece of data like name has to pass through several layers of components just to reach a deeply nested UserName component.

Now scale that tree up, with multiple pieces of data threading through components that don't even use them directly.

That pattern is known as prop drilling.

React's Context API offers a way around this: it lets you make data available anywhere deeper in the tree without manually forwarding it through every component in between.

const ParentContext = createContext(null)
<ParentContext value={money}>
  <ChildComponent />
</ ParentContext>
function ChildComponent() {
  const money = useContext(ParentContext);

  return <p>Money: {money}</p>;
}

There are many more hooks beyond these, and it's worth experimenting with them on your own.

Up to this point, the discussion has been about how React organizes components and how data moves between them. But a real application also has to decide which parts of that interface should show up under which URLs.

React Router

React lets you build interfaces made of components that update on their own, without forcing the browser to reload the entire page. But a real application usually needs more than one page, and that raises a new question.

What happens once your app needs several distinct views?

You might want something like:

/home → Home /dashboard → Dashboard /profile → Profile

If you wire these up with plain HTML `` tags, clicking them triggers a full browser navigation. The whole page gets thrown away and the app boots up again from scratch at the new address.

What you actually want is for the URL to update while React quietly figures out which components need to swap out, without discarding everything else.

That's the problem React Router solves.

Think of it as a layer that maps URLs to components.

For instance:

<BrowserRouter>
  <Routes>
    <Route path="/home" element={<Home />} />
    <Route path="/dashboard" element={<Dashboard />} />
  </Routes>
</BrowserRouter>

React Router inspects the current URL and renders whatever component is tied to it. BrowserRouter relies on the browser's History API to handle navigation on the client side, and Routes picks out whichever Route best matches the current path.

Even so, there's another wrinkle to deal with.

What if you don't want the whole screen to re-render?

Picture a layout like this:

┌─────────────────────────────┐
│          Header             │
├──────────┬──────────────────┤
│          │                  │
│  Menu    │   Page content   │
│          │                  │
├──────────┴──────────────────┤
│          Footer             │
└─────────────────────────────┘

Moving from /home to /dashboard shouldn't make the header, sidebar, or footer vanish and reappear. Only the main content area needs to change.

That's exactly the scenario nested routes and <Outlet /> are built for.

function Layout() {
  return (
    <>
      <Header />
      <Menu />
      <Outlet />
      <Footer />
    </>
  );
}

The routes themselves can then be nested inside one another:

<Routes>
  <Route element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="dashboard" element={<Dashboard />} />
  </Route>
</Routes>

With this setup, Layout remains mounted, and React Router swaps in whichever child route matches inside the <Outlet />. According to React Router's own documentation, <Outlet /> marks the spot where the matching child route gets rendered.

The index route matches <Home /> to the / path, making it the default view shown inside the outlet when no more specific path is active.

So going from /home to /dashboard isn't really about replacing the entire page. It's closer to saying:

"Leave this part of the interface as it is, and swap out just this section for whatever component belongs to the new route."

What next?

Once you have a solid grasp of the problems React exists to solve and the mechanisms it uses to solve them, it's worth spending time reading through real codebases to see how experienced teams structure their applications.

Look for a repository that demonstrates how a production React application can be organized and architected at scale.

Rather than trying to absorb the whole codebase in one sitting, pick a single feature and follow it through the layers of the app. Notice how components are grouped, where the data originates, how state is handled, and how separate parts of the app talk to one another.

It's also worth finding an example that shows how React can be paired with Redux in a working application.

If the project you find is older, don't treat its patterns as the current standard for React. Use it instead to study how a large application can be broken into pieces and how Redux slots into that structure.

Once you're comfortable navigating a typical React file layout and can put together simple components on your own, the next step is learning to build applications that hold up at scale.

Scaling a React application introduces its own set of concerns, including:

  • SSR and Server Components — how behavior changes when portions of the app run on the server rather than entirely in the browser.
  • State management — what to do once your application's state grows too large or too widely shared for local state and Context to handle comfortably. Redux is one option among several.
  • Data fetching and caching — how production apps manage loading indicators, error handling, caching, and keeping client data in sync with the server.
  • Performance — recognizing when rendering genuinely becomes a bottleneck, and optimizing accordingly instead of optimizing everything up front.
  • Testing — verifying that components and user flows keep working correctly as the codebase grows larger.

You don't need to master every one of these topics before you start building.

A more practical approach is to start building, run into a specific problem, and then learn whatever concept or tool solves that particular problem.

Ultimately, the point of learning React was never to memorize its API surface. It was to understand why those APIs exist and how to reason about the problems they were designed to address.