--- title: React as a UI Runtime date: '2019-02-02' spoiler: An in-depth description of the React programming model. cta: 'react' --- Most tutorials introduce React as a UI library. This makes sense because React *is* a UI library. That’s literally what the tagline says! ![React homepage screenshot: "A JavaScript library for building user interfaces"](./react.png) I’ve written about the challenges of creating [user interfaces](/the-elements-of-ui-engineering/) before. But this post talks about React in a different way — more as a [programming runtime](https://en.wikipedia.org/wiki/Runtime_system). **This post won’t teach you anything about creating user interfaces.** But it might help you understand the React programming model in more depth. --- **Note: If you’re _learning_ React, check out [the docs](https://reactjs.org/docs/getting-started.html#learn-react) instead.**

⚠️

**This is a deep dive — THIS IS NOT a beginner-friendly post.** In this post, I’m describing most of the React programming model from first principles. I don’t explain how to use it — just how it works. It’s aimed at experienced programmers and folks working on other UI libraries who asked about some tradeoffs chosen in React. I hope you’ll find it useful! **Many people successfully use React for years without thinking about most of these topics.** This is definitely a programmer-centric view of React rather than, say, a [designer-centric one](http://mrmrs.cc/writing/developing-ui/). But I don’t think it hurts to have resources for both. With that disclaimer out of the way, let’s go! --- ## Host Tree Some programs output numbers. Other programs output poems. Different languages and their runtimes are often optimized for a particular set of use cases, and React is no exception to that. React programs usually output **a tree that may change over time**. It might be a [DOM tree](https://www.npmjs.com/package/react-dom), an [iOS hierarchy](https://developer.apple.com/library/archive/documentation/General/Conceptual/Devpedia-CocoaApp/View%20Hierarchy.html), a tree of [PDF primitives](https://react-pdf.org/), or even of [JSON objects](https://reactjs.org/docs/test-renderer.html). However, usually, we want to represent some UI with it. We’ll call it a “*host* tree” because it is a part of the *host environment* outside of React — like DOM or iOS. The host tree usually has [its](https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild) [own](https://developer.apple.com/documentation/uikit/uiview/1622616-addsubview) imperative API. React is a layer on top of it. So what is React useful for? Very abstractly, it helps you write a program that predictably manipulates a complex host tree in response to external events like interactions, network responses, timers, and so on. A specialized tool works better than a generic one when it can impose and benefit from particular constraints. React makes a bet on two principles: * **Stability.** The host tree is relatively stable and most updates don’t radically change its overall structure. If an app rearranged all its interactive elements into a completely different combination every second, it would be difficult to use. Where did that button go? Why is my screen dancing? * **Regularity.** The host tree can be broken down into UI patterns that look and behave consistently (such as buttons, lists, avatars) rather than random shapes. **These principles happen to be true for most UIs.** However, React is ill-suited when there are no stable “patterns” in the output. For example, React may help you write a Twitter client but won’t be very useful for a [3D pipes screensaver](https://www.youtube.com/watch?v=Uzx9ArZ7MUU). ## Host Instances The host tree consists of nodes. We’ll call them “host instances”. In the DOM environment, host instances are regular DOM nodes — like the objects you get when you call `document.createElement('div')`. On iOS, host instances could be values uniquely identifying a native view from JavaScript. Host instances have their own properties (e.g. `domNode.className` or `view.tintColor`). They may also contain other host instances as children. (This has nothing to do with React — I’m describing the host environments.) There is usually an API to manipulate host instances. For example, the DOM provides APIs like `appendChild`, `removeChild`, `setAttribute`, and so on. In React apps, you usually don’t call these APIs. That’s the job of React. ## Renderers A *renderer* teaches React to talk to a specific host environment and manage its host instances. React DOM, React Native, and even [Ink](https://mobile.twitter.com/vadimdemedes/status/1089344289102942211) are React renderers. You can also [create your own React renderer](https://github.com/facebook/react/tree/master/packages/react-reconciler). React renderers can work in one of two modes. The vast majority of renderers are written to use the “mutating” mode. This mode is how the DOM works: we can create a node, set its properties, and later add or remove children from it. The host instances are completely mutable. React can also work in a [“persistent”](https://en.wikipedia.org/wiki/Persistent_data_structure) mode. This mode is for host environments that don’t provide methods like `appendChild()` but instead clone the parent tree and always replace the top-level child. Immutability on the host tree level makes multi-threading easier. [React Fabric](https://facebook.github.io/react-native/blog/2018/06/14/state-of-react-native-2018) takes advantage of that. As a React user, you never need to think about these modes. I only want to highlight that React isn’t just an adapter from one mode to another. Its usefulness is orthogonal to the target low-level view API paradigm. ## React Elements In the host environment, a host instance (like a DOM node) is the smallest building block. In React, the smallest building block is a *React element*. A React element is a plain JavaScript object. It can *describe* a host instance. ```jsx // JSX is a syntax sugar for these objects. // ); } ``` It returns a pair of values: the current state and a function that updates it. The [array destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Array_destructuring) syntax lets us give arbitrary names to our state variables. For example, I called this pair `count` and `setCount`, but it could’ve been `banana` and `setBanana`. In the text below, I will use `setState` to refer to the second value regardless of its actual name in the specific examples. *(You can learn more about `useState` and other Hooks provided by React [here](https://reactjs.org/docs/hooks-intro.html).)* ## Consistency Even if we want to split the reconciliation process itself into [non-blocking](https://www.youtube.com/watch?v=mDdgfyRB5kg) chunks of work, we should still perform the actual host tree operations in a single synchronous swoop. This way we can ensure that the user doesn’t see a half-updated UI, and that the browser doesn’t perform unnecessary layout and style recalculation for intermediate states that the user shouldn’t see. This is why React splits all work into the “render phase” and the “commit phase”. *Render phase* is when React calls your components and performs reconciliation. It is safe to interrupt and [in the future](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html) will be asynchronous. *Commit phase* is when React touches the host tree. It is always synchronous. ## Memoization When a parent schedules an update by calling `setState`, by default React reconciles its whole child subtree. This is because React can’t know whether an update in the parent would affect the child or not, and by default, React opts to be consistent. This may sound very expensive but in practice, it’s not a problem for small and medium-sized subtrees. When trees get too deep or wide, you can tell React to [memoize](https://en.wikipedia.org/wiki/Memoization) a subtree and reuse previous render results during shallow equal prop changes: ```jsx {5} function Row({ item }) { // ... } export default React.memo(Row); ``` Now `setState` in a parent `` component would skip over reconciling `Row`s whose `item` is referentially equal to the `item` rendered last time. You can get fine-grained memoization at the level of individual expressions with the [`useMemo()` Hook](https://reactjs.org/docs/hooks-reference.html#usememo). The cache is local to component tree position and will be destroyed together with its local state. It only holds one last item. React intentionally doesn’t memoize components by default. Many components always receive different props so memoizing them would be a net loss. ## Raw Models Ironically, React doesn’t use a “reactivity” system for fine-grained updates. In other words, any update at the top triggers reconciliation instead of updating just the components affected by changes. This is an intentional design decision. [Time to interactive](https://calibreapp.com/blog/time-to-interactive/) is a crucial metric in consumer web applications, and traversing models to set up fine-grained listeners spends that precious time. Additionally, in many apps, interactions tend to result either in small (button hover) or large (page transition) updates, in which case fine-grained subscriptions are a waste of memory resources. One of the core design principles of React is that it works with raw data. If you have a bunch of JavaScript objects received from the network, you can pump them directly into your components with no preprocessing. There are no gotchas about which properties you can access, or unexpected performance cliffs when a structure slightly changes. React rendering is O(*view size*) rather than O(*model size*), and you can significantly cut the *view size* with [windowing](https://react-window.now.sh/#/examples/list/fixed-size). There are some kinds of applications where fine-grained subscriptions are beneficial — such as stock tickers. This is a rare example of “everything constantly updating at the same time”. While imperative escape hatches can help optimize such code, React might not be the best fit for this use case. Still, you can implement your own fine-grained subscription system on top of React. **Note that there are common performance issues that even fine-grained subscriptions and “reactivity” systems can’t solve.** For example, rendering a *new* deep tree (which happens on every page transition) without blocking the browser. Change tracking doesn’t make it faster — it makes it slower because we have to do more work to set up subscriptions. Another problem is that we have to wait for data before we can start rendering the view. In React, we aim to solve both of these problems with [Concurrent Rendering](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html). ## Batching Several components may want to update state in response to the same event. This example is contrived but it illustrates a common pattern: ```jsx {4,14} function Parent() { let [count, setCount] = useState(0); return (
setCount(count + 1)}> Parent clicked {count} times
); } function Child() { let [count, setCount] = useState(0); return ( ); } ``` When an event is dispatched, the child’s `onClick` fires first (triggering its `setState`). Then the parent calls `setState` in its own `onClick` handler. If React immediately re-rendered components in response to `setState` calls, we’d end up rendering the child twice: ```jsx {4,8} *** Entering React's browser click event handler *** Child (onClick) - setState - re-render Child // 😞 unnecessary Parent (onClick) - setState - re-render Parent - re-render Child *** Exiting React's browser click event handler *** ``` The first `Child` render would be wasted. And we couldn’t make React skip rendering `Child` for the second time because the `Parent` might pass some different data to it based on its updated state. **This is why React batches updates inside event handlers:** ```jsx *** Entering React's browser click event handler *** Child (onClick) - setState Parent (onClick) - setState *** Processing state updates *** - re-render Parent - re-render Child *** Exiting React's browser click event handler *** ``` The `setState` calls in components wouldn’t immediately cause a re-render. Instead, React would execute all event handlers first, and then trigger a single re-render batching all of those updates together. Batching is good for performance but can be surprising if you write code like: ```jsx const [count, setCount] = useState(0); function increment() { setCount(count + 1); } function handleClick() { increment(); increment(); increment(); } ``` If we start with `count` set to `0`, these would just be three `setCount(1)` calls. To fix this, `setState` provides an overload that accepts an “updater” function: ```jsx const [count, setCount] = useState(0); function increment() { setCount(c => c + 1); } function handleClick() { increment(); increment(); increment(); } ``` React would put the updater functions in a queue, and later run them in sequence, resulting in a re-render with `count` set to `3`. When state logic gets more complex than a few `setState` calls, I recommend expressing it as a local state reducer with the [`useReducer` Hook](https://reactjs.org/docs/hooks-reference.html#usereducer). It’s like an evolution of this “updater” pattern where each update is given a name: ```jsx const [counter, dispatch] = useReducer((state, action) => { if (action === 'increment') { return state + 1; } else { return state; } }, 0); function handleClick() { dispatch('increment'); dispatch('increment'); dispatch('increment'); } ``` The `action` argument can be anything, although an object is a common choice. ## Call Tree A programming language runtime usually has a [call stack](https://medium.freecodecamp.org/understanding-the-javascript-call-stack-861e41ae61d4). When a function `a()` calls `b()` which itself calls `c()`, somewhere in the JavaScript engine there’s a data structure like `[a, b, c]` that “keeps track” of where you are and what code to execute next. Once you exit out of `c`, its call stack frame is gone — poof! It’s not needed anymore. We jump back into `b`. By the time we exit `a`, the call stack is empty. Of course, React itself runs in JavaScript and obeys JavaScript rules. But we can imagine that internally React has some kind of its own call stack to remember which component we are currently rendering, e.g. `[App, Page, Layout, Article /* we're here */]`. React is different from a general purpose language runtime because it’s aimed at rendering UI trees. These trees need to “stay alive” for us to interact with them. The DOM doesn’t disappear after our first `ReactDOM.render()` call. This may be stretching the metaphor but I like to think of React components as being in a “call tree” rather than just a “call stack”. When we go “out” of the `Article` component, its React “call tree” frame doesn’t get destroyed. We need to keep the local state and references to the host instances [somewhere](https://medium.com/react-in-depth/the-how-and-why-on-reacts-usage-of-linked-list-in-fiber-67f1014d0eb7). These “call tree” frames *are* destroyed along with their local state and host instances, but only when the [reconciliation](#reconciliation) rules say it’s necessary. If you ever read React source, you might have seen these frames being referred to as [Fibers](https://en.wikipedia.org/wiki/Fiber_(computer_science)). Fibers are where the local state actually lives. When the state is updated, React marks the Fibers below as needing reconciliation, and calls those components. ## Context In React, we pass things down to other components as props. Sometimes, the majority of components need the same thing — for example, the currently chosen visual theme. It gets cumbersome to pass it down through every level. In React, this is solved by [Context](https://reactjs.org/docs/context.html). It is essentially like [dynamic scoping](http://wiki.c2.com/?DynamicScoping) for components. It’s like a wormhole that lets you put something on the top, and have every child at the bottom be able to read it and re-render when it changes. ```jsx const ThemeContext = React.createContext( 'light' // Default value as a fallback ); function DarkApp() { return ( ); } function SomeDeeplyNestedChild() { // Depends on where the child is rendered const theme = useContext(ThemeContext); // ... } ``` When `SomeDeeplyNestedChild` renders, `useContext(ThemeContext)` will look for the closest `` above it in the tree, and use its `value`. (In practice, React maintains a context stack while it renders.) If there’s no `ThemeContext.Provider` above, the result of `useContext(ThemeContext)` call will be the default value specified in the `createContext()` call. In our example, it is `'light'`. ## Effects We mentioned earlier that React components shouldn’t have observable side effects during rendering. But side effects are sometimes necessary. We may want to manage focus, draw on a canvas, subscribe to a data source, and so on. In React, this is done by declaring an effect: ```jsx {4-6} function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return (

You clicked {count} times

); } ``` When possible, React defers executing effects until after the browser re-paints the screen. This is good because code like data source subscriptions shouldn’t hurt [time to interactive](https://calibreapp.com/blog/time-to-interactive/) and [time to first paint](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint). (There's a [rarely used](https://reactjs.org/docs/hooks-reference.html#uselayouteffect) Hook that lets you opt out of that behavior and do things synchronously. Avoid it.) Effects don’t just run once. They run both after a component is shown to the user for the first time, and after it updates. Effects can close over current props and state, such as with `count` in the above example. Effects may require cleanup, such as in case of subscriptions. To clean up after itself, an effect can return a function: ```jsx useEffect(() => { DataSource.addSubscription(handleChange); return () => DataSource.removeSubscription(handleChange); }); ``` React will execute the returned function before applying this effect the next time, and also before the component is destroyed. Sometimes, re-running the effect on every render can be undesirable. You can tell React to [skip](https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects) applying an effect if certain variables didn’t change: ```jsx {3} useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); ``` However, it is often a premature optimization and can lead to problems if you’re not familiar with how JavaScript closures work. For example, this code is buggy: ```jsx useEffect(() => { DataSource.addSubscription(handleChange); return () => DataSource.removeSubscription(handleChange); }, []); ``` It is buggy because `[]` says “don’t ever re-execute this effect”. But the effect closes over `handleChange` which is defined outside of it. And `handleChange` might reference any props or state: ```jsx function handleChange() { console.log(count); } ``` If we never let the effect re-run, `handleChange` will keep pointing at the version from the first render, and `count` will always be `0` inside of it. To solve this, make sure that if you specify the dependency array, it includes **all** things that can change, including the functions: ```jsx {4} useEffect(() => { DataSource.addSubscription(handleChange); return () => DataSource.removeSubscription(handleChange); }, [handleChange]); ``` Depending on your code, you might still see unnecessary resubscriptions because `handleChange` itself is different on every render. The [`useCallback`](https://reactjs.org/docs/hooks-reference.html#usecallback) Hook can help you with that. Alternatively, you can just let it re-subscribe. For example, browser’s `addEventListener` API is extremely fast, and jumping through hoops to avoid calling it might cause more problems than it’s worth. *(You can learn more about `useEffect` and other Hooks provided by React [here](https://reactjs.org/docs/hooks-effect.html).)* ## Custom Hooks Since Hooks like `useState` and `useEffect` are function calls, we can compose them into our own Hooks: ```jsx {2,8} function MyResponsiveComponent() { const width = useWindowWidth(); // Our custom Hook return (

Window width is {width}

); } function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }); return width; } ``` Custom Hooks let different components share reusable stateful logic. Note that the *state itself* is not shared. Each call to a Hook declares its own isolated state. *(You can learn more about writing your own Hooks [here](https://reactjs.org/docs/hooks-custom.html).)* ## Static Use Order You can think of `useState` as a syntax for defining a “React state variable”. It’s not *really* a syntax, of course. We’re still writing JavaScript. But we are looking at React as a runtime environment, and because React tailors JavaScript to describing UI trees, its features sometimes live closer to the language space. If `use` *were* a syntax, it would make sense for it to be at the top level: ```jsx {3} // 😉 Note: not a real syntax component Example(props) { const [count, setCount] = use State(0); return (

You clicked {count} times

); } ``` What would putting it into a condition or a callback or outside a component even mean? ```jsx // 😉 Note: not a real syntax // This is local state... of what? const [count, setCount] = use State(0); component Example() { if (condition) { // What happens to it when condition is false? const [count, setCount] = use State(0); } function handleClick() { // What happens to it when we leave a function? // How is this different from a variable? const [count, setCount] = use State(0); } ``` React state is local to the *component* and its identity in the tree. If `use` were a real syntax it would make sense to scope it to the top-level of a component too: ```jsx // 😉 Note: not a real syntax component Example(props) { // Only valid here const [count, setCount] = use State(0); if (condition) { // This would be a syntax error const [count, setCount] = use State(0); } ``` This is similar to how `import` only works at the top level of a module. **Of course, `use` is not actually a syntax.** (It wouldn’t bring much benefit and would create a lot of friction.) However, React *does* expect that all calls to Hooks happen only at the top level of a component and unconditionally. These [Rules of Hooks](https://reactjs.org/docs/hooks-rules.html) can be enforced with [a linter plugin](https://www.npmjs.com/package/eslint-plugin-react-hooks). There have been heated arguments about this design choice but in practice, I haven’t seen it confusing people. I also wrote about why commonly proposed alternatives [don’t work](/why-do-hooks-rely-on-call-order/). Internally, Hooks are implemented as [linked lists](https://dev.to/aspittel/thank-u-next-an-introduction-to-linked-lists-4pph). When you call `useState`, we move the pointer to the next item. When we exit the component’s [“call tree” frame](#call-tree), we save the resulting list there until the next render. [This article](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e) provides a simplified explanation for how Hooks work internally. Arrays might be an easier mental model than linked lists: ```jsx // Pseudocode let hooks, i; function useState() { i++; if (hooks[i]) { // Next renders return hooks[i]; } // First render hooks.push(...); } // Prepare to render i = -1; hooks = fiber.hooks || []; // Call the component YourComponent(); // Remember the state of Hooks fiber.hooks = hooks; ``` *(If you’re curious, the real code is [here](https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.new.js).)* This is roughly how each `useState()` call gets the right state. As we’ve learned [earlier](#reconciliation), “matching things up” isn’t new to React — reconciliation relies on the elements matching up between renders in a similar way. ## What’s Left Out We’ve touched on pretty much all important aspects of the React runtime environment. If you finished this page, you probably know React in more detail than 90% of its users. And there’s nothing wrong with that! There are some parts I left out — mostly because they’re unclear even to us. React doesn’t currently have a good story for multipass rendering, i.e. when the parent render needs information about the children. Also, the [error handling API](https://reactjs.org/docs/error-boundaries.html) doesn’t yet have a Hooks version. It’s possible that these two problems can be solved together. Concurrent Mode is not stable yet, and there are interesting questions about how Suspense fits into this picture. Maybe I’ll do a follow-up when they’re fleshed out and Suspense is ready for more than [lazy loading](https://reactjs.org/blog/2018/10/23/react-v-16-6.html#reactlazy-code-splitting-with-suspense). **I think it speaks to the success of React’s API that you can get very far without ever thinking about most of these topics.** Good defaults like the reconciliation heuristics do the right thing in most cases. Warnings, like the `key` warning, nudge you when you risk shooting yourself in the foot. If you’re a UI library nerd, I hope this post was somewhat entertaining and clarified how React works in more depth. Or maybe you decided React is too complicated and you’ll never look at it again. In either case, I’d love to hear from you on Twitter! Thank you for reading.