I’ve spent the last few weeks digging around inside React’s code, trying to figure out how it does what it does. And while I was prepping for my talk with Uri Shaked about Angular Ivy and React Fiber, I had the chance to throw some debugger statements in the code and see what happens. Turns out you can get pretty deep inside a framework with nothing but the developer tools. In fact — you can understand almost everything about it.
So today I’m going to show you how I dug into React’s inner workings with just some debugger statements, and hopefully convince you that you can do it too. We’re going to focus on one question — what happens to the partial state we pass to
setState? Where does it go?
The idea is to treat this process as I would in real life, if I was trying to figure it out for the first time. With one main exception: I’ve done this before. Multiple times (so, so many times). And so I will take some shortcuts and add some background information, so that this post can be a reasonable length (because I love you!). But believe me when I tell you — I learned almost all of this from iterating over the process I show here, and some from external resources which I will link to. And so can you.
If you want to check out the app I work with in this post, and possibly throw in some debugger statements of your own, you can do so via the link below. Though fair warning — it is probably the least useful app I have ever written.
We are going to be following
setState to see what happens with the partial state we pass to it. In order to do that, I’m going to do something you wouldn’t likely do in real-life: I’ll call
setState from within a
I’m doing this because the alternative way to trigger a state change without too much fuss would have been via an event handler. However, since React uses Event Delegation, this flow results in a slightly different call stack which will make it more difficult for us to see what’s going on. That’s why I’ve created this contrived example where
setState is triggered externally. So our app begins with
this.state.pet:🐿 ️, and about 1 second later the callback will run, and this will change to
When I first started looking through React internals, the second most-common question I asked myself was “what do I do now?” (the first was of course “what the fuck?”). Over time I realized that I can always use something I already know, and build on top of it to go one step further. For instance, in this case we know that whenever we call
setState, this eventually triggers a lifecycle method, the most obvious one being
So I should be able to put a debugger statement in my
componentDidUpdate method and see what happens in between those two points.
Now when the page reloads, the execution will pause at the debugger statement. By looking at the screen I can tell that the component has already been updated with the new state, and the DOM has already been re-rendered. A look inside the call stack can reveal the path React took to get from
setState to here.
First Look at the Call Stack
If you check out our call stack now, it may look a bit daunting. That’s OK. It’s still just a call stack. Some of those functions might seem foreign, but we can also find some anchors we recognize — whether it’s
setState, or the later functions that have to do with lifecycles.
performWorkOnRoot is where most of the magic happens — this is the place where React initiates it’s rendering phase that is described in Lin Clark’s wonderful Fiber talk. So if you really want to get your hands dirty you might consider setting a few breakpoints in that bad boy and seeing what happens.
We’re going to focus on following our state, and the first function that gets called after
enqueueSetState. So let’s see what happens there:
Its arguments are
callback. A quick glimpse inside
setState (the function which calls it) will show us that these are respectively the component instance, the partial state passed to
setState and the callback passed to
setState, if there is one.
Now the first thing this function does is retrieve the component’s Fiber. If you’re unfamiliar with Fibers, check out this great in-depth explanation: https://github.com/acdlite/react-fiber-architecture, but for our purposes, let’s use this mental model — a Fiber is an internal representation of a component that React uses to do its reconciliation. All Fibers are linked together in a structure called a linked-list-tree, which is designed to allow for easy and efficient traversal between Fibers that need to share data. Here, in
enqueueSetState, we can clearly see the relationship between the Fiber and the component instance.
Next, React sets the variables
expirationTime, and we’re going to take React’s word for what it does there, and not delve into the implementation details. Then it creates a variable called
createUpdate, and saves our
payload on it. The
payload argument stores our partial state, so we should definitely look into that. Let’s take a peek inside our
The first thing we can see here, is that the
payload is in fact the partial state we had passed to
setState. And we can see something else — that the expiration time for this update is not actually a time, or some big number representing a timestamp like we might expect, but 1. This is because React treats this as a synchronous update, which means it’s not going to do all that Fiber magic of stopping mid-work to yield control to the browser. I can’t tell you for sure why React does that — but my educated guess is it has to do with the fact this call came from within a lifecycle method, and React wants it to execute as soon as possible, without pausing.
We can elegantly ignore the next few lines, as they have to do with the
callback parameter, which we didn’t pass a callback to our
setState, I’m going to elegantly ignore those lines of code and move on. I’ve figured out that my partial state went into the
update object, so now I want to follow that and see what React does with it, and the next thing it does is call
If we follow this function back to where it’s defined, the first thing we can see is that React reads a property called
alternate off the Fiber. This function has already returned, so we could add a breakpoint here and rerun, but we still have access to our Fiber so another option is to print out the value of
fiber.alternate in the console. Compare and contrast the Fiber and the Alternate for a bit and you will find this — the
alternate is itself a Fiber that is a representation of the same component, with slight variations from our current Fiber. If you look more closely at the differences you might be able to spot this:
Keep in mind that at this point in time our component and view have already been updated, meaning that React has finished its reconciliation work and this is the final state of these two Fibers. Our takeaway from this, is one Fiber represents the previous state, and the other represents the current state.
Let’s look back to
enqueueUpdate for a minute. What else does it do? Well, it seems to do some checks on the two Fibers, to see which of them exists, and also tests a specific property on the Fibers, the
In fact, if we read through the function we’ll see that the
update, which includes our partial state, is appended to one or both of these queues. Since we have access to the final versions of these two Fibers, we can compare them and see what each
updateQueue looks like:
It seems the Alternate has nothing much in it’s queue, but it does have the final state saved in its
baseState property. In contrast, the Fiber we started out with has the previous state as its
baseState, but it also has a
lastUpdate, which look familiar…
Yep, that’s right, it’s our state update, saved as the first (and last) update in this Fiber’s update queue. If we fill in the blanks, we can infer that React must have saved the new state as part of the first Fiber’s
updateQueue, and then did something to process it — likely iterated over the queue — and created the final state in the Alternate.
And in fact, had we dug in a bit deeper and looked inside React’s reconciliation algorithm — specifically at its Render Phase and the
beginWork function — we would have found exactly that. What’s more — we would have seen that React uses these Fibers alternately (a huge clue into that is that the Alternate’s
alternate property points to the original Fiber, so they are interchangeable). To be precise — at almost all times there are two identical Fiber trees in React, and it uses one to keep a reference to the old state, and one to create the new state.
React still has a lot more work to do with updating other Fibers, the DOM and so on… but for now we’ve managed to answer our big question. So what happens to the partial state? It gets saved to the Fiber’s queue, and is then used to update the state of the Fiber’s
alternate. While figuring this out — we’ve learned a couple of things about how React operates internally. And we did this with nothing but our developer tools and logic.
No magic necessary.
I leave you with this; you deserve it!