On this page, we will primarily use function components. However, these testing strategies don’t depend on implementation details, and work just as well for class components too.
Setup/Teardown
act()
Rendering
Data Fetching
Mocking Modules
Events
Timers
Snapshot Testing
Multiple Renderers
Something Missing?
Setup/Teardown
For each test, we usually want to render our React tree to a DOM element that’s attached to
document
. This is important so that it can receive DOM events. When the test ends, we want to “clean up” and unmount the tree from the
document
.
A common way to do it is to use a pair of
beforeEach
and
afterEach
blocks so that they’ll always run and isolate the effects of a test to itself:
import{ unmountComponentAtNode }from"react-dom";
let container =null; beforeEach(()=>{ // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); });
afterEach(()=>{ // cleanup on exiting unmountComponentAtNode(container); container.remove(); container =null; });
You may use a different pattern, but keep in mind that we want to execute the cleanup
even if a test fails
. Otherwise, tests can become “leaky”, and one test can change the behavior of another test. That makes them difficult to debug.
act()
When writing UI tests, tasks like rendering, user events, or data fetching can be considered as “units” of interaction with a user interface.
react-dom/test-utils
provides a helper called
act()
that makes sure all updates related to these “units” have been processed and applied to the DOM before you make any assertions:
act(()=>{ // render components }); // make assertions
This helps make your tests run closer to what real users would experience when using your application. The rest of these examples use
act()
to make these guarantees.
You might find using
act()
directly a bit too verbose. To avoid some of the boilerplate, you could use a library like React Testing Library, whose helpers are wrapped with
act()
.
Note:
The name
act
comes from the Arrange-Act-Assert pattern.
Rendering
Commonly, you might want to test whether a component renders correctly for given props. Consider a simple component that renders a message based on a prop:
let container =null; beforeEach(()=>{ // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); });
afterEach(()=>{ // cleanup on exiting unmountComponentAtNode(container); container.remove(); container =null; });
it("renders with or without a name",()=>{ act(()=>{render(<Hello/>, container);});expect(container.textContent).toBe("Hey, stranger"); act(()=>{ render(<Helloname="Jenny"/>, container); }); expect(container.textContent).toBe("Hello, Jenny!");
Instead of calling real APIs in all your tests, you can mock requests with dummy data. Mocking data fetching with “fake” data prevents flaky tests due to an unavailable backend, and makes them run faster. Note: you may still want to run a subset of tests using an “end-to-end” framework that tells whether the whole app is working together.
let container =null; beforeEach(()=>{ // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); });
afterEach(()=>{ // cleanup on exiting unmountComponentAtNode(container); container.remove(); container =null; });
it("renders user data",async()=>{ const fakeUser ={name:"Joni Baez",age:"32",address:"123, Charming Avenue"}; jest.spyOn(global,"fetch").mockImplementation(()=> Promise.resolve({json:()=> Promise.resolve(fakeUser)})); // Use the asynchronous version of act to apply resolved promises awaitact(async()=>{ render(<Userid="123"/>, container); });
// remove the mock to ensure tests are completely isolated global.fetch.mockRestore();});
Mocking Modules
Some modules might not work well inside a testing environment, or may not be as essential to the test itself. Mocking out these modules with dummy replacements can make it easier to write tests for your own code.
Consider a
Contact
component that embeds a third-party
GoogleMap
component:
jest.mock("./map",()=>{returnfunctionDummyMap(props){return(<divdata-testid="map">{props.center.lat}:{props.center.long}</div>);};}); let container =null; beforeEach(()=>{ // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); });
afterEach(()=>{ // cleanup on exiting unmountComponentAtNode(container); container.remove(); container =null; });
let container =null; beforeEach(()=>{ // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container);}); afterEach(()=>{ // cleanup on exiting unmountComponentAtNode(container); container.remove(); container =null; });
it("changes value when clicked",()=>{ const onChange = jest.fn(); act(()=>{ render(<ToggleonChange={onChange}/>, container); });
// get a hold of the button element, and trigger some clicks on it const button = document.querySelector("[data-testid=toggle]"); expect(button.innerHTML).toBe("Turn on");
Different DOM events and their properties are described in MDN. Note that you need to pass
{ bubbles: true }
in each event you create for it to reach the React listener because React automatically delegates events to the root.
Note:
React Testing Library offers a more concise helper for firing events.
Timers
Your code might use timer-based functions like
setTimeout
to schedule more work in the future. In this example, a multiple choice panel waits for a selection and advances, timing out if a selection isn’t made in 5 seconds:
import Card from"./card"; let container =null; beforeEach(()=>{ // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); jest.useFakeTimers(); });
You can use fake timers only in some tests. Above, we enabled them by calling
jest.useFakeTimers()
. The main advantage they provide is that your test doesn’t actually have to wait five seconds to execute, and you also didn’t need to make the component code more convoluted just for testing.
Snapshot Testing
Frameworks like Jest also let you save “snapshots” of data with
toMatchSnapshot
/
toMatchInlineSnapshot
. With these, we can “save” the rendered component output and ensure that a change to it has to be explicitly committed as a change to the snapshot.
In this example, we render a component and format the rendered HTML with the
pretty
package, before saving it as an inline snapshot:
let container =null; beforeEach(()=>{ // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); });
afterEach(()=>{ // cleanup on exiting unmountComponentAtNode(container); container.remove(); container =null; });
it("should render a greeting",()=>{ act(()=>{ render(<Hello/>, container); });
expect(pretty(container.innerHTML)).toMatchInlineSnapshot();/* ... gets filled automatically by jest ... */ act(()=>{ render(<Helloname="Jenny"/>, container); });
expect( pretty(container.innerHTML) ).toMatchInlineSnapshot();/* ... gets filled automatically by jest ... */
expect( pretty(container.innerHTML) ).toMatchInlineSnapshot();/* ... gets filled automatically by jest ... */ });
It’s typically better to make more specific assertions than to use snapshots. These kinds of tests include implementation details so they break easily, and teams can get desensitized to snapshot breakages. Selectively mocking some child components can help reduce the size of snapshots and keep them readable for the code review.
Multiple Renderers
In rare cases, you may be running a test on a component that uses multiple renderers. For example, you may be running snapshot tests on a component with
react-test-renderer
, that internally uses
render
from
react-dom
inside a child component to render some content. In this scenario, you can wrap updates with
act()
s corresponding to their renderers.
import{ act as domAct }from"react-dom/test-utils"; import{ act as testAct, create }from"react-test-renderer"; // ... let root; domAct(()=>{ testAct(()=>{ root =create(<App/>); }); }); expect(root).toMatchSnapshot();
Something Missing?
If some common scenario is not covered, please let us know on the issue tracker for the documentation website.
Is this page useful?
Edit this page
Testing Environments – React
Testing Environments
This document goes through the factors that can affect your environment and recommendations for some scenarios.
Test runners like Jest, mocha, ava let you write test suites as regular JavaScript, and run them as part of your development process. Additionally, test suites are run as part of continuous integration.
Jest is widely compatible with React projects, supporting features like mocked modules and timers, and
jsdom
support.
If you use Create React App, Jest is already included out of the box with useful defaults.
Libraries like mocha work well in real browser environments, and could help for tests that explicitly need it.
End-to-end tests are used for testing longer flows across multiple pages, and require a different setup.
Mocking a rendering surface
Tests often run in an environment without access to a real rendering surface like a browser. For these environments, we recommend simulating a browser with
jsdom
, a lightweight browser implementation that runs inside Node.js.
In most cases, jsdom behaves like a regular browser would, but doesn’t have features like layout and navigation. This is still useful for most web-based component tests, since it runs quicker than having to start up a browser for each test. It also runs in the same process as your tests, so you can write code to examine and assert on the rendered DOM.
Just like in a real browser, jsdom lets us model user interactions; tests can dispatch events on DOM nodes, and then observe and assert on the side effects of these actions
(example)
.
A large portion of UI tests can be written with the above setup: using Jest as a test runner, rendered to jsdom, with user interactions specified as sequences of browser events, powered by the
act()
helper
(example)
. For example, a lot of React’s own tests are written with this combination.
If you’re writing a library that tests mostly browser-specific behavior, and requires native browser behavior like layout or real inputs, you could use a framework like mocha.
In an environment where you
can’t
simulate a DOM (e.g. testing React Native components on Node.js), you could use event simulation helpers to simulate interactions with elements. Alternately, you could use the
fireEvent
helper from
@testing-library/react-native
.
Frameworks like Cypress, puppeteer and webdriver are useful for running end-to-end tests.
Mocking functions
When writing tests, we’d like to mock out the parts of our code that don’t have equivalents inside our testing environment (e.g. checking
navigator.onLine
status inside Node.js). Tests could also spy on some functions, and observe how other parts of the test interact with them. It is then useful to be able to selectively mock these functions with test-friendly versions.
This is especially useful for data fetching. It is usually preferable to use “fake” data for tests to avoid the slowness and flakiness due to fetching from real API endpoints
(example)
. This helps make the tests predictable. Libraries like Jest and sinon, among others, support mocked functions. For end-to-end tests, mocking network can be more difficult, but you might also want to test the real API endpoints in them anyway.
Mocking modules
Some components have dependencies for modules that may not work well in test environments, or aren’t essential to our tests. It can be useful to selectively mock these modules out with suitable replacements
(example)
.
On Node.js, runners like Jest support mocking modules. You could also use libraries like
mock-require
.
Mocking timers
Components might be using time-based functions like
setTimeout
,
setInterval
, or
Date.now
. In testing environments, it can be helpful to mock these functions out with replacements that let you manually “advance” time. This is great for making sure your tests run fast! Tests that are dependent on timers would still resolve in order, but quicker
(example)
. Most frameworks, including Jest, sinon and lolex, let you mock timers in your tests.
Sometimes, you may not want to mock timers. For example, maybe you’re testing an animation, or interacting with an endpoint that’s sensitive to timing (like an API rate limiter). Libraries with timer mocks let you enable and disable them on a per test/suite basis, so you can explicitly choose how these tests would run.
End-to-end tests
End-to-end tests are useful for testing longer workflows, especially when they’re critical to your business (such as payments or signups). For these tests, you’d probably want to test how a real browser renders the whole app, fetches data from the real API endpoints, uses sessions and cookies, navigates between different links. You might also likely want to make assertions not just on the DOM state, but on the backing data as well (e.g. to verify whether the updates have been persisted to the database).
In this scenario, you would use a framework like Cypress, Playwright or a library like Puppeteer so you can navigate between multiple routes and assert on side effects not just in the browser, but potentially on the backend as well.
Facebook has adopted the Contributor Covenant as its Code of Conduct, and we expect project participants to adhere to it. Please read the full text so that you can understand what actions will and will not be tolerated.
Open Development
All work on React happens directly on GitHub. Both core team members and external contributors send pull requests which go through the same review process.
Semantic Versioning
React follows semantic versioning. We release patch versions for critical bugfixes, minor versions for new features or non-essential changes, and major versions for any breaking changes. When we make breaking changes, we also introduce deprecation warnings in a minor version so that our users learn about the upcoming changes and migrate their code in advance. Learn more about our commitment to stability and incremental migration in our versioning policy.
Every significant change is documented in the changelog file.
Branch Organization
Submit all changes directly to the
main branch
. We don’t use separate branches for development or for upcoming releases. We do our best to keep
main
in good shape, with all tests passing.
Code that lands in
main
must be compatible with the latest stable release. It may contain additional features, but no breaking changes. We should be able to release a new minor version from the tip of
main
at any time.
Feature Flags
To keep the
main
branch in a releasable state, breaking changes and experimental features must be gated behind a feature flag.
Feature flags are defined in
packages/shared/ReactFeatureFlags.js
. Some builds of React may enable different sets of feature flags; for example, the React Native build may be configured differently than React DOM. These flags are found in
packages/shared/forks
. Feature flags are statically typed by Flow, so you can run
yarn flow
to confirm that you’ve updated all the necessary files.
React’s build system will strip out disabled feature branches before publishing. A continuous integration job runs on every commit to check for changes in bundle size. You can use the change in size as a signal that a feature was gated correctly.
Bugs
Where to Find Known Issues
We are using GitHub Issues for our public bugs. We keep a close eye on this and try to make it clear when we have an internal fix in progress. Before filing a new task, try to make sure your problem doesn’t already exist.
Reporting New Issues
The best way to get your bug fixed is to provide a reduced test case. This JSFiddle template is a great starting point.
Security Bugs
Facebook has a bounty program for the safe disclosure of security bugs. With that in mind, please do not file public issues; go through the process outlined on that page.
How to Get in Touch
IRC: #reactjs on freenode
Discussion forums
There is also an active community of React users on the Discord chat platform in case you need help with React.
Proposing a Change
If you intend to change the public API, or make any non-trivial changes to the implementation, we recommend filing an issue. This lets us reach an agreement on your proposal before you put significant effort into it.
If you’re only fixing a bug, it’s fine to submit a pull request right away but we still recommend to file an issue detailing what you’re fixing. This is helpful in case we don’t accept that specific fix but want to keep track of the issue.
Your First Pull Request
Working on your first Pull Request? You can learn how from this free video series:
How to Contribute to an Open Source Project on GitHub
To help you get your feet wet and get you familiar with our contribution process, we have a list of
good first issues
that contain bugs that have a relatively limited scope. This is a great place to get started.
If you decide to fix an issue, please be sure to check the comment thread in case somebody is already working on a fix. If nobody is working on it at the moment, please leave a comment stating that you intend to work on it so other people don’t accidentally duplicate your effort.
If somebody claims an issue but doesn’t follow up for more than two weeks, it’s fine to take it over but you should still leave a comment.
Sending a Pull Request
The core team is monitoring for pull requests. We will review your pull request and either merge it, request changes to it, or close it with an explanation. For API changes we may need to fix our internal uses at Facebook.com, which could cause some delay. We’ll do our best to provide updates and feedback throughout the process.
Before submitting a pull request,
please make sure the following is done:
Fork the repository and create your branch from
main
.
Run
yarn
in the repository root.
If you’ve fixed a bug or added code that should be tested, add tests!
Ensure the test suite passes (
yarn test
). Tip:
yarn test --watch TestName
is helpful in development.
Run
yarn test --prod
to test in the production environment.
If you need a debugger, run
yarn debug-test --watch TestName
, open
chrome://inspect
, and press “Inspect”.
Format your code with prettier (
yarn prettier
).
Make sure your code lints (
yarn lint
). Tip:
yarn linc
to only check changed files.
Run the Flow typechecks (
yarn flow
).
If you haven’t already, complete the CLA.
Contributor License Agreement (CLA)
In order to accept your pull request, we need you to submit a CLA. You only need to do this once, so if you’ve done this for another Facebook open source project, you’re good to go. If you are submitting a pull request for the first time, just let us know that you have completed the CLA and we can cross-check with your GitHub username.
Complete your CLA here.
Contribution Prerequisites
You have Node installed at LTS and Yarn at v1.2.0+.
You have JDK installed.
You have
gcc
installed or are comfortable installing a compiler if needed. Some of our dependencies may require a compilation step. On OS X, the Xcode Command Line Tools will cover this. On Ubuntu,
apt-get install build-essential
will install the required packages. Similar commands should work on other Linux distros. Windows will require some additional steps, see the
node-gyp
installation instructions for details.
You are familiar with Git.
Development Workflow
After cloning React, run
yarn
to fetch its dependencies.
Then, you can run several commands:
yarn lint
checks the code style.
yarn linc
is like
yarn lint
but faster because it only checks files that differ in your branch.
yarn test
runs the complete test suite.
yarn test --watch
runs an interactive test watcher.
yarn test --prod
runs tests in the production environment.
yarn test <pattern>
runs tests with matching filenames.
yarn debug-test
is just like
yarn test
but with a debugger. Open
chrome://inspect
and press “Inspect”.
yarn flow
runs the Flow typechecks.
yarn build
creates a
build
folder with all the packages.
yarn build react/index,react-dom/index --type=UMD
creates UMD builds of just React and ReactDOM.
We recommend running
yarn test
(or its variations above) to make sure you don’t introduce any regressions as you work on your change. However, it can be handy to try your build of React in a real project.
First, run
yarn build
. This will produce pre-built bundles in
build
folder, as well as prepare npm packages inside
build/packages
.
The easiest way to try your changes is to run
yarn build react/index,react-dom/index --type=UMD
and then open
fixtures/packaging/babel-standalone/dev.html
. This file already uses
react.development.js
from the
build
folder so it will pick up your changes.
If you want to try your changes in your existing React project, you may copy
build/node_modules/react/umd/react.development.js
,
build/node_modules/react-dom/umd/react-dom.development.js
, or any other build products into your app and use them instead of the stable version.
If your project uses React from npm, you may delete
react
and
react-dom
in its dependencies and use
yarn link
to point them to your local
build
folder. Note that
instead of
--type=UMD
you’ll want to pass
--type=NODE
when building
. You’ll also need to build the
scheduler
package:
cd ~/path_to_your_react_clone/ yarn build react/index,react/jsx,react-dom/index,scheduler --type=NODE
cd build/node_modules/react yarn link cd build/node_modules/react-dom yarn link
cd ~/path/to/your/project yarn link react react-dom
Every time you run
yarn build
in the React folder, the updated versions will appear in your project’s
node_modules
. You can then rebuild your project to try your changes.
If some package is still missing (e.g. maybe you use
react-dom/server
in your project), you can always do a full build with
yarn build
. Note that running
yarn build
without options takes a long time.
We still require that your pull request contains unit tests for any new functionality. This way we can ensure that we don’t break your code in the future.
Style Guide
We use an automatic code formatter called Prettier.
Run
yarn prettier
after making any changes to the code.
Then, our linter will catch most issues that may exist in your code.
You can check the status of your code styling by simply running
yarn linc
.
However, there are still some styles that the linter cannot pick up. If you are unsure about something, looking at Airbnb’s Style Guide will guide you in the right direction.
Request for Comments (RFC)
Many changes, including bug fixes and documentation improvements can be implemented and reviewed via the normal GitHub pull request workflow.
Some changes though are “substantial”, and we ask that these be put through a bit of a design process and produce a consensus among the React core team.
The “RFC” (request for comments) process is intended to provide a consistent and controlled path for new features to enter the project. You can contribute by visiting the rfcs repository.
License
By contributing to React, you agree that your contributions will be licensed under its MIT license.
What Next?
Read the next section to learn how the codebase is organized.
We don’t necessarily recommend any of these conventions in React apps. Many of them exist for historical reasons and might change with time.
Top-Level Folders
After cloning the React repository, you will see a few top-level folders in it:
packages
contains metadata (such as
package.json
) and the source code (
src
subdirectory) for all packages in the React repository.
If your change is related to the code, the
src
subdirectory of each package is where you’ll spend most of your time.
fixtures
contains a few small React test applications for contributors.
build
is the build output of React. It is not in the repository but it will appear in your React clone after you build it for the first time.
The documentation is hosted in a separate repository from React.
There are a few other top-level folders but they are mostly used for the tooling and you likely won’t ever encounter them when contributing.
Colocated Tests
We don’t have a top-level directory for unit tests. Instead, we put them into a directory called
__tests__
relative to the files that they test.
For example, a test for
setInnerHTML.js
is located in
__tests__/setInnerHTML-test.js
right next to it.
Warnings and Invariants
The React codebase uses
console.error
to display warnings:
if(__DEV__){ console.error('Something is wrong.'); }
Warnings are only enabled in development. In production, they are completely stripped out. If you need to forbid some code path from executing, use
invariant
module instead:
var invariant =require('invariant');
invariant( 2+2===4, 'You shall not pass!' );
The invariant is thrown when the
invariant
condition is
false
.
“Invariant” is just a way of saying “this condition always holds true”. You can think about it as making an assertion.
It is important to keep development and production behavior similar, so
invariant
throws both in development and in production. The error messages are automatically replaced with error codes in production to avoid negatively affecting the byte size.
Development and Production
You can use
__DEV__
pseudo-global variable in the codebase to guard development-only blocks of code.
It is inlined during the compile step, and turns into
process.env.NODE_ENV !== 'production'
checks in the CommonJS builds.
For standalone builds, it becomes
true
in the unminified build, and gets completely stripped out with the
if
blocks it guards in the minified build.
if(__DEV__){ // This code will only run in development. }
Flow
We recently started introducing Flow checks to the codebase. Files marked with the
@flow
annotation in the license header comment are being typechecked.
We accept pull requests adding Flow annotations to existing code. Flow annotations look like this:
When possible, new code should use Flow annotations.
You can run
yarn flow
locally to check your code with Flow.
Multiple Packages
React is a monorepo. Its repository contains multiple separate packages so that their changes can be coordinated together, and issues live in one place.
React Core
The “core” of React includes all the top-level
React
APIs, for example:
React.createElement()
React.Component
React.Children
React core only includes the APIs necessary to define components.
It does not include the reconciliation algorithm or any platform-specific code. It is used both by React DOM and React Native components.
The code for React core is located in
packages/react
in the source tree. It is available on npm as the
react
package. The corresponding standalone browser build is called
react.js
, and it exports a global called
React
.
Renderers
React was originally created for the DOM but it was later adapted to also support native platforms with React Native. This introduced the concept of “renderers” to React internals.
Renderers manage how a React tree turns into the underlying platform calls.
Renderers are also located in
packages/
:
React DOM Renderer renders React components to the DOM. It implements top-level
ReactDOM
APIs and is available as
react-dom
npm package. It can also be used as standalone browser bundle called
react-dom.js
that exports a
ReactDOM
global.
React Native Renderer renders React components to native views. It is used internally by React Native.
React Test Renderer renders React components to JSON trees. It is used by the Snapshot Testing feature of Jest and is available as react-test-renderer npm package.
The only other officially supported renderer is
react-art
. It used to be in a separate GitHub repository but we moved it into the main source tree for now.
Note:
Technically the
react-native-renderer
is a very thin layer that teaches React to interact with React Native implementation. The real platform-specific code managing the native views lives in the React Native repository together with its components.
Reconcilers
Even vastly different renderers like React DOM and React Native need to share a lot of logic. In particular, the reconciliation algorithm should be as similar as possible so that declarative rendering, custom components, state, lifecycle methods, and refs work consistently across platforms.
To solve this, different renderers share some code between them. We call this part of React a “reconciler”. When an update such as
setState()
is scheduled, the reconciler calls
render()
on components in the tree and mounts, updates, or unmounts them.
Reconcilers are not packaged separately because they currently have no public API. Instead, they are exclusively used by renderers such as React DOM and React Native.
Stack Reconciler
The “stack” reconciler is the implementation powering React 15 and earlier. We have since stopped using it, but it is documented in detail in the next section.
Fiber Reconciler
The “fiber” reconciler is a new effort aiming to resolve the problems inherent in the stack reconciler and fix a few long-standing issues. It has been the default reconciler since React 16.
Its main goals are:
Ability to split interruptible work in chunks.
Ability to prioritize, rebase and reuse work in progress.
Ability to yield back and forth between parents and children to support layout in React.
Ability to return multiple elements from
render()
.
Better support for error boundaries.
You can read more about React Fiber Architecture here and here. While it has shipped with React 16, the async features are not enabled by default yet.
Its source code is located in
packages/react-reconciler
.
Event System
React implements a layer over native events to smooth out cross-browser differences. Its source code is located in
packages/react-dom/src/events
.
What Next?
Read the next section to learn about the pre-React 16 implementation of reconciler in more detail. We haven’t documented the internals of the new reconciler yet.
This section is a collection of implementation notes for the
stack reconciler.
It is very technical and assumes a strong understanding of React public API as well as how it’s divided into core, renderers, and the reconciler. If you’re not very familiar with the React codebase, read the codebase overview first.
It also assumes an understanding of the differences between React components, their instances, and elements.
The stack reconciler was used in React 15 and earlier. It is located at src/renderers/shared/stack/reconciler.
Video: Building React from Scratch
Paul O’Shannessy gave a talk about building React from scratch that largely inspired this document.
Both this document and his talk are simplifications of the real codebase so you might get a better understanding by getting familiar with both of them.
Overview
The reconciler itself doesn’t have a public API. Renderers like React DOM and React Native use it to efficiently update the user interface according to the React components written by the user.
Mounting as a Recursive Process
Let’s consider the first time you mount a component:
root.render
will pass
<App />
along to the reconciler. Remember that
<App />
is a React element, that is, a description of
what
to render. You can think about it as a plain object:
console.log(<App/>); // { type: App, props: {} }
The reconciler will check if
App
is a class or a function.
If
App
is a function, the reconciler will call
App(props)
to get the rendered element.
If
App
is a class, the reconciler will instantiate an
App
with
new App(props)
, call the
componentWillMount()
lifecycle method, and then will call the
render()
method to get the rendered element.
Either way, the reconciler will learn the element
App
“rendered to”.
This process is recursive.
App
may render to a
<Greeting />
,
Greeting
may render to a
<Button />
, and so on. The reconciler will “drill down” through user-defined components recursively as it learns what each component renders to.
You can imagine this process as a pseudocode:
functionisClass(type){ // React.Component subclasses have this flag return( Boolean(type.prototype)&& Boolean(type.prototype.isReactComponent) ); }
// This function takes a React element (e.g. <App />) // and returns a DOM or Native node representing the mounted tree. functionmount(element){ var type = element.type; var props = element.props;
// We will determine the rendered element // by either running the type as function // or creating an instance and calling render(). var renderedElement; if(isClass(type)){ // Component class var publicInstance =newtype(props); // Set the props publicInstance.props = props; // Call the lifecycle if necessary if(publicInstance.componentWillMount){ publicInstance.componentWillMount(); } // Get the rendered element by calling render() renderedElement = publicInstance.render(); }else{ // Component function renderedElement =type(props); }
// This process is recursive because a component may // return an element with a type of another component. returnmount(renderedElement);
// Note: this implementation is incomplete and recurses infinitely! // It only handles elements like <App /> or <Button />. // It doesn't handle elements like <div /> or <p /> yet. }
var rootEl = document.getElementById('root'); var node =mount(<App/>); rootEl.appendChild(node);
Note:
This really
is
a pseudo-code. It isn’t similar to the real implementation. It will also cause a stack overflow because we haven’t discussed when to stop the recursion.
Let’s recap a few key ideas in the example above:
React elements are plain objects representing the component type (e.g.
App
) and the props.
User-defined components (e.g.
App
) can be classes or functions but they all “render to” elements.
“Mounting” is a recursive process that creates a DOM or Native tree given the top-level React element (e.g.
<App />
).
Mounting Host Elements
This process would be useless if we didn’t render something to the screen as a result.
In addition to user-defined (“composite”) components, React elements may also represent platform-specific (“host”) components. For example,
Button
might return a
<div />
from its render method.
If element’s
type
property is a string, we are dealing with a host element:
There is no user-defined code associated with host elements.
When the reconciler encounters a host element, it lets the renderer take care of mounting it. For example, React DOM would create a DOM node.
If the host element has children, the reconciler recursively mounts them following the same algorithm as above. It doesn’t matter whether children are host (like
<div><hr /></div>
), composite (like
<div><Button /></div>
), or both.
The DOM nodes produced by the child components will be appended to the parent DOM node, and recursively, the complete DOM structure will be assembled.
Note:
The reconciler itself is not tied to the DOM. The exact result of mounting (sometimes called “mount image” in the source code) depends on the renderer, and can be a DOM node (React DOM), a string (React DOM Server), or a number representing a native view (React Native).
If we were to extend the code to handle host elements, it would look like this:
functionisClass(type){ // React.Component subclasses have this flag return( Boolean(type.prototype)&& Boolean(type.prototype.isReactComponent) ); }
// This function only handles elements with a composite type. // For example, it handles <App /> and <Button />, but not a <div />. functionmountComposite(element){ var type = element.type; var props = element.props;
var renderedElement; if(isClass(type)){ // Component class var publicInstance =newtype(props); // Set the props publicInstance.props = props; // Call the lifecycle if necessary if(publicInstance.componentWillMount){ publicInstance.componentWillMount(); } renderedElement = publicInstance.render(); }elseif(typeof type ==='function'){ // Component function renderedElement =type(props); }
// This is recursive but we'll eventually reach the bottom of recursion when // the element is host (e.g. <div />) rather than composite (e.g. <App />): returnmount(renderedElement); }
// This function only handles elements with a host type. // For example, it handles <div /> and <p /> but not an <App />. functionmountHost(element){ var type = element.type; var props = element.props; var children = props.children ||[]; if(!Array.isArray(children)){ children =[children]; } children = children.filter(Boolean);
// This block of code shouldn't be in the reconciler. // Different renderers might initialize nodes differently. // For example, React Native would create iOS or Android views. var node = document.createElement(type); Object.keys(props).forEach(propName=>{ if(propName !=='children'){ node.setAttribute(propName, props[propName]); } });
// Mount the children children.forEach(childElement=>{ // Children may be host (e.g. <div />) or composite (e.g. <Button />). // We will also mount them recursively: var childNode =mount(childElement);
// This line of code is also renderer-specific. // It would be different depending on the renderer: node.appendChild(childNode); });
// Return the DOM node as mount result. // This is where the recursion ends. return node; }
functionmount(element){ var type = element.type; if(typeof type ==='function'){ // User-defined components returnmountComposite(element); }elseif(typeof type ==='string'){ // Platform-specific components returnmountHost(element); } }
var rootEl = document.getElementById('root'); var node =mount(<App/>); rootEl.appendChild(node);
This is working but still far from how the reconciler is really implemented. The key missing ingredient is support for updates.
Introducing Internal Instances
The key feature of React is that you can re-render everything, and it won’t recreate the DOM or reset the state:
root.render(<App/>); // Should reuse the existing DOM: root.render(<App/>);
However, our implementation above only knows how to mount the initial tree. It can’t perform updates on it because it doesn’t store all the necessary information, such as all the
publicInstance
s, or which DOM
node
s correspond to which components.
The stack reconciler codebase solves this by making the
mount()
function a method and putting it on a class. There are drawbacks to this approach, and we are going in the opposite direction in the ongoing rewrite of the reconciler. Nevertheless this is how it works now.
Instead of separate
mountHost
and
mountComposite
functions, we will create two classes:
DOMComponent
and
CompositeComponent
.
Both classes have a constructor accepting the
element
, as well as a
mount()
method returning the mounted node. We will replace a top-level
mount()
function with a factory that instantiates the correct class:
functioninstantiateComponent(element){ var type = element.type; if(typeof type ==='function'){ // User-defined components returnnewCompositeComponent(element); }elseif(typeof type ==='string'){ // Platform-specific components returnnewDOMComponent(element); } }
First, let’s consider the implementation of
CompositeComponent
:
getPublicInstance(){ // For composite components, expose the class instance. returnthis.publicInstance; }
mount(){ var element =this.currentElement; var type = element.type; var props = element.props;
var publicInstance; var renderedElement; if(isClass(type)){ // Component class publicInstance =newtype(props); // Set the props publicInstance.props = props; // Call the lifecycle if necessary if(publicInstance.componentWillMount){ publicInstance.componentWillMount(); } renderedElement = publicInstance.render(); }elseif(typeof type ==='function'){ // Component function publicInstance =null; renderedElement =type(props); }
// Save the public instance this.publicInstance = publicInstance;
// Instantiate the child internal instance according to the element. // It would be a DOMComponent for <div /> or <p />, // and a CompositeComponent for <App /> or <Button />: var renderedComponent =instantiateComponent(renderedElement); this.renderedComponent = renderedComponent;
// Mount the rendered output return renderedComponent.mount(); } }
This is not much different from our previous
mountComposite()
implementation, but now we can save some information, such as
this.currentElement
,
this.renderedComponent
, and
this.publicInstance
, for use during updates.
Note that an instance of
CompositeComponent
is not the same thing as an instance of the user-supplied
element.type
.
CompositeComponent
is an implementation detail of our reconciler, and is never exposed to the user. The user-defined class is the one we read from
element.type
, and
CompositeComponent
creates an instance of it.
To avoid the confusion, we will call instances of
CompositeComponent
and
DOMComponent
“internal instances”. They exist so we can associate some long-lived data with them. Only the renderer and the reconciler are aware that they exist.
In contrast, we call an instance of the user-defined class a “public instance”. The public instance is what you see as
this
in the
render()
and other methods of your custom components.
The
mountHost()
function, refactored to be a
mount()
method on
DOMComponent
class, also looks familiar:
getPublicInstance(){ // For DOM components, only expose the DOM node. returnthis.node; }
mount(){ var element =this.currentElement; var type = element.type; var props = element.props; var children = props.children ||[]; if(!Array.isArray(children)){ children =[children]; }
// Create and save the node var node = document.createElement(type); this.node = node;
// Set the attributes Object.keys(props).forEach(propName=>{ if(propName !=='children'){ node.setAttribute(propName, props[propName]); } });
// Create and save the contained children. // Each of them can be a DOMComponent or a CompositeComponent, // depending on whether the element type is a string or a function. var renderedChildren = children.map(instantiateComponent); this.renderedChildren = renderedChildren;
// Collect DOM nodes they return on mount var childNodes = renderedChildren.map(child=> child.mount()); childNodes.forEach(childNode=> node.appendChild(childNode));
// Return the DOM node as mount result return node; } }
The main difference after refactoring from
mountHost()
is that we now keep
this.node
and
this.renderedChildren
associated with the internal DOM component instance. We will also use them for applying non-destructive updates in the future.
As a result, each internal instance, composite or host, now points to its child internal instances. To help visualize this, if a function
<App>
component renders a
<Button>
class component, and
Button
class renders a
<div>
, the internal instance tree would look like this:
In the DOM you would only see the
<div>
. However the internal instance tree contains both composite and host internal instances.
The composite internal instances need to store:
The current element.
The public instance if element type is a class.
The single rendered internal instance. It can be either a
DOMComponent
or a
CompositeComponent
.
The host internal instances need to store:
The current element.
The DOM node.
All the child internal instances. Each of them can be either a
DOMComponent
or a
CompositeComponent
.
If you’re struggling to imagine how an internal instance tree is structured in more complex applications, React DevTools can give you a close approximation, as it highlights host instances with grey, and composite instances with purple:
To complete this refactoring, we will introduce a function that mounts a complete tree into a container node and a public instance:
functionmountTree(element, containerNode){ // Create the top-level internal instance var rootComponent =instantiateComponent(element);
// Mount the top-level component into the container var node = rootComponent.mount(); containerNode.appendChild(node);
// Return the public instance it provides var publicInstance = rootComponent.getPublicInstance(); return publicInstance; }
var rootEl = document.getElementById('root'); mountTree(<App/>, rootEl);
Unmounting
Now that we have internal instances that hold onto their children and the DOM nodes, we can implement unmounting. For a composite component, unmounting calls a lifecycle method and recurses.
classCompositeComponent{
// ...
unmount(){ // Call the lifecycle method if necessary var publicInstance =this.publicInstance; if(publicInstance){ if(publicInstance.componentWillUnmount){ publicInstance.componentWillUnmount(); } }
// Unmount the single rendered component var renderedComponent =this.renderedComponent; renderedComponent.unmount(); } }
For
DOMComponent
, unmounting tells each child to unmount:
classDOMComponent{
// ...
unmount(){ // Unmount all the children var renderedChildren =this.renderedChildren; renderedChildren.forEach(child=> child.unmount()); } }
In practice, unmounting DOM components also removes the event listeners and clears some caches, but we will skip those details.
We can now add a new top-level function called
unmountTree(containerNode)
that is similar to
ReactDOM.unmountComponentAtNode()
:
functionunmountTree(containerNode){ // Read the internal instance from a DOM node: // (This doesn't work yet, we will need to change mountTree() to store it.) var node = containerNode.firstChild; var rootComponent = node._internalInstance;
// Unmount the tree and clear the container rootComponent.unmount(); containerNode.innerHTML =''; }
In order for this to work, we need to read an internal root instance from a DOM node. We will modify
mountTree()
to add the
_internalInstance
property to the root DOM node. We will also teach
mountTree()
to destroy any existing tree so it can be called multiple times:
functionmountTree(element, containerNode){ // Destroy any existing tree if(containerNode.firstChild){ unmountTree(containerNode); }
// Create the top-level internal instance var rootComponent =instantiateComponent(element);
// Mount the top-level component into the container var node = rootComponent.mount(); containerNode.appendChild(node);
// Save a reference to the internal instance node._internalInstance = rootComponent;
// Return the public instance it provides var publicInstance = rootComponent.getPublicInstance(); return publicInstance; }
Now, running
unmountTree()
, or running
mountTree()
repeatedly, removes the old tree and runs the
componentWillUnmount()
lifecycle method on components.
Updating
In the previous section, we implemented unmounting. However React wouldn’t be very useful if each prop change unmounted and mounted the whole tree. The goal of the reconciler is to reuse existing instances where possible to preserve the DOM and the state:
var rootEl = document.getElementById('root');
mountTree(<App/>, rootEl); // Should reuse the existing DOM: mountTree(<App/>, rootEl);
We will extend our internal instance contract with one more method. In addition to
mount()
and
unmount()
, both
DOMComponent
and
CompositeComponent
will implement a new method called
receive(nextElement)
:
classCompositeComponent{ // ...
receive(nextElement){ // ... } }
classDOMComponent{ // ...
receive(nextElement){ // ... } }
Its job is to do whatever is necessary to bring the component (and any of its children) up to date with the description provided by the
nextElement
.
This is the part that is often described as “virtual DOM diffing” although what really happens is that we walk the internal tree recursively and let each internal instance receive an update.
Updating Composite Components
When a composite component receives a new element, we run the
componentWillUpdate()
lifecycle method.
Then we re-render the component with the new props, and get the next rendered element:
classCompositeComponent{
// ...
receive(nextElement){ var prevProps =this.currentElement.props; var publicInstance =this.publicInstance; var prevRenderedComponent =this.renderedComponent; var prevRenderedElement = prevRenderedComponent.currentElement;
// Update *own* element this.currentElement = nextElement; var type = nextElement.type; var nextProps = nextElement.props;
// Figure out what the next render() output is var nextRenderedElement; if(isClass(type)){ // Component class // Call the lifecycle if necessary if(publicInstance.componentWillUpdate){ publicInstance.componentWillUpdate(nextProps); } // Update the props publicInstance.props = nextProps; // Re-render nextRenderedElement = publicInstance.render(); }elseif(typeof type ==='function'){ // Component function nextRenderedElement =type(nextProps); }
// ...
Next, we can look at the rendered element’s
type
. If the
type
has not changed since the last render, the component below can also be updated in place.
For example, if it returned
<Button color="red" />
the first time, and
<Button color="blue" />
the second time, we can just tell the corresponding internal instance to
receive()
the next element:
// ...
// If the rendered element type has not changed, // reuse the existing component instance and exit. if(prevRenderedElement.type === nextRenderedElement.type){ prevRenderedComponent.receive(nextRenderedElement); return; }
// ...
However, if the next rendered element has a different
type
than the previously rendered element, we can’t update the internal instance. A
<button>
can’t “become” an
<input>
.
Instead, we have to unmount the existing internal instance and mount the new one corresponding to the rendered element type. For example, this is what happens when a component that previously rendered a
<button />
renders an
<input />
:
// ...
// If we reached this point, we need to unmount the previously // mounted component, mount the new one, and swap their nodes.
// Find the old node because it will need to be replaced var prevNode = prevRenderedComponent.getHostNode();
// Unmount the old child and mount a new child prevRenderedComponent.unmount(); var nextRenderedComponent =instantiateComponent(nextRenderedElement); var nextNode = nextRenderedComponent.mount();
// Replace the reference to the child this.renderedComponent = nextRenderedComponent;
// Replace the old node with the new one // Note: this is renderer-specific code and // ideally should live outside of CompositeComponent: prevNode.parentNode.replaceChild(nextNode, prevNode); } }
To sum this up, when a composite component receives a new element, it may either delegate the update to its rendered internal instance, or unmount it and mount a new one in its place.
There is another condition under which a component will re-mount rather than receive an element, and that is when the element’s
key
has changed. We don’t discuss
key
handling in this document because it adds more complexity to an already complex tutorial.
Note that we needed to add a method called
getHostNode()
to the internal instance contract so that it’s possible to locate the platform-specific node and replace it during the update. Its implementation is straightforward for both classes:
classCompositeComponent{ // ...
getHostNode(){ // Ask the rendered component to provide it. // This will recursively drill down any composites. returnthis.renderedComponent.getHostNode(); } }
classDOMComponent{ // ...
getHostNode(){ returnthis.node; } }
Updating Host Components
Host component implementations, such as
DOMComponent
, update differently. When they receive an element, they need to update the underlying platform-specific view. In case of React DOM, this means updating the DOM attributes:
classDOMComponent{ // ...
receive(nextElement){ var node =this.node; var prevElement =this.currentElement; var prevProps = prevElement.props; var nextProps = nextElement.props; this.currentElement = nextElement;
// Remove old attributes. Object.keys(prevProps).forEach(propName=>{ if(propName !=='children'&&!nextProps.hasOwnProperty(propName)){ node.removeAttribute(propName); } }); // Set next attributes. Object.keys(nextProps).forEach(propName=>{ if(propName !=='children'){ node.setAttribute(propName, nextProps[propName]); } });
// ...
Then, host components need to update their children. Unlike composite components, they might contain more than a single child.
In this simplified example, we use an array of internal instances and iterate over it, either updating or replacing the internal instances depending on whether the received
type
matches their previous
type
. The real reconciler also takes element’s
key
in the account and track moves in addition to insertions and deletions, but we will omit this logic.
We collect DOM operations on children in a list so we can execute them in batch:
// ...
// These are arrays of React elements: var prevChildren = prevProps.children ||[]; if(!Array.isArray(prevChildren)){ prevChildren =[prevChildren]; } var nextChildren = nextProps.children ||[]; if(!Array.isArray(nextChildren)){ nextChildren =[nextChildren]; } // These are arrays of internal instances: var prevRenderedChildren =this.renderedChildren; var nextRenderedChildren =[];
// As we iterate over children, we will add operations to the array. var operationQueue =[];
// Note: the section below is extremely simplified! // It doesn't handle reorders, children with holes, or keys. // It only exists to illustrate the overall flow, not the specifics.
for(var i =0; i < nextChildren.length; i++){ // Try to get an existing internal instance for this child var prevChild = prevRenderedChildren[i];
// If there is no internal instance under this index, // a child has been appended to the end. Create a new // internal instance, mount it, and use its node. if(!prevChild){ var nextChild =instantiateComponent(nextChildren[i]); var node = nextChild.mount();
// Record that we need to append a node operationQueue.push({type:'ADD', node}); nextRenderedChildren.push(nextChild); continue; }
// We can only update the instance if its element's type matches. // For example, <Button size="small" /> can be updated to // <Button size="large" /> but not to an <App />. var canUpdate = prevChildren[i].type === nextChildren[i].type;
// If we can't update an existing instance, we have to unmount it // and mount a new one instead of it. if(!canUpdate){ var prevNode = prevChild.getHostNode(); prevChild.unmount();
var nextChild =instantiateComponent(nextChildren[i]); var nextNode = nextChild.mount();
// Record that we need to swap the nodes operationQueue.push({type:'REPLACE', prevNode, nextNode}); nextRenderedChildren.push(nextChild); continue; }
// If we can update an existing internal instance, // just let it receive the next element and handle its own update. prevChild.receive(nextChildren[i]); nextRenderedChildren.push(prevChild); }
// Finally, unmount any children that don't exist: for(var j = nextChildren.length; j < prevChildren.length; j++){ var prevChild = prevRenderedChildren[j]; var node = prevChild.getHostNode(); prevChild.unmount();
// Record that we need to remove the node operationQueue.push({type:'REMOVE', node}); }
// Point the list of rendered children to the updated version. this.renderedChildren = nextRenderedChildren;
// ...
As the last step, we execute the DOM operations. Again, the real reconciler code is more complex because it also handles moves:
// ...
// Process the operation queue. while(operationQueue.length >0){ var operation = operationQueue.shift(); switch(operation.type){ case'ADD': this.node.appendChild(operation.node); break; case'REPLACE': this.node.replaceChild(operation.nextNode, operation.prevNode); break; case'REMOVE': this.node.removeChild(operation.node); break; } } } }
And that is it for updating host components.
Top-Level Updates
Now that both
CompositeComponent
and
DOMComponent
implement the
receive(nextElement)
method, we can change the top-level
mountTree()
function to use it when the element
type
is the same as it was the last time:
functionmountTree(element, containerNode){ // Check for an existing tree if(containerNode.firstChild){ var prevNode = containerNode.firstChild; var prevRootComponent = prevNode._internalInstance; var prevElement = prevRootComponent.currentElement;
// If we can, reuse the existing root component if(prevElement.type === element.type){ prevRootComponent.receive(element); return; }
// Otherwise, unmount the existing tree unmountTree(containerNode); }
// ...
}
Now calling
mountTree()
two times with the same type isn’t destructive:
var rootEl = document.getElementById('root');
mountTree(<App/>, rootEl); // Reuses the existing DOM: mountTree(<App/>, rootEl);
These are the basics of how React works internally.
What We Left Out
This document is simplified compared to the real codebase. There are a few important aspects we didn’t address:
Components can render
null
, and the reconciler can handle “empty slots” in arrays and rendered output.
The reconciler also reads
key
from the elements, and uses it to establish which internal instance corresponds to which element in an array. A bulk of complexity in the actual React implementation is related to that.
In addition to composite and host internal instance classes, there are also classes for “text” and “empty” components. They represent text nodes and the “empty slots” you get by rendering
null
.
Renderers use injection to pass the host internal class to the reconciler. For example, React DOM tells the reconciler to use
ReactDOMComponent
as the host internal instance implementation.
The logic for updating the list of children is extracted into a mixin called
ReactMultiChild
which is used by the host internal instance class implementations both in React DOM and React Native.
The reconciler also implements support for
setState()
in composite components. Multiple updates inside event handlers get batched into a single update.
The reconciler also takes care of attaching and detaching refs to composite components and host nodes.
Lifecycle methods that are called after the DOM is ready, such as
componentDidMount()
and
componentDidUpdate()
, get collected into “callback queues” and are executed in a single batch.
React puts information about the current update into an internal object called “transaction”. Transactions are useful for keeping track of the queue of pending lifecycle methods, the current DOM nesting for the warnings, and anything else that is “global” to a specific update. Transactions also ensure React “cleans everything up” after updates. For example, the transaction class provided by React DOM restores the input selection after any update.
Jumping into the Code
ReactMount
is where the code like
mountTree()
and
unmountTree()
from this tutorial lives. It takes care of mounting and unmounting top-level components.
ReactNativeMount
is its React Native analog.
ReactDOMComponent
is the equivalent of
DOMComponent
in this tutorial. It implements the host component class for React DOM renderer.
ReactNativeBaseComponent
is its React Native analog.
ReactCompositeComponent
is the equivalent of
CompositeComponent
in this tutorial. It handles calling user-defined components and maintaining their state.
instantiateReactComponent
contains the switch that picks the right internal instance class to construct for an element. It is equivalent to
instantiateComponent()
in this tutorial.
ReactReconciler
is a wrapper with
mountComponent()
,
receiveComponent()
, and
unmountComponent()
methods. It calls the underlying implementations on the internal instances, but also includes some code around them that is shared by all internal instance implementations.
ReactChildReconciler
implements the logic for mounting, updating, and unmounting children according to the
key
of their elements.
ReactMultiChild
implements processing the operation queue for child insertions, deletions, and moves independently of the renderer.
mount()
,
receive()
, and
unmount()
are really called
mountComponent()
,
receiveComponent()
, and
unmountComponent()
in React codebase for legacy reasons, but they receive elements.
Properties on the internal instances start with an underscore, e.g.
_currentElement
. They are considered to be read-only public fields throughout the codebase.
Future Directions
Stack reconciler has inherent limitations such as being synchronous and unable to interrupt the work or split it in chunks. There is a work in progress on the new Fiber reconciler with a completely different architecture. In the future, we intend to replace stack reconciler with it, but at the moment it is far from feature parity.
Next Steps
Read the next section to learn about the guiding principles we use for React development.
We wrote this document so that you have a better idea of how we decide what React does and what React doesn’t do, and what our development philosophy is like. While we are excited to see community contributions, we are not likely to choose a path that violates one or more of these principles.
Note:
This document assumes a strong understanding of React. It describes the design principles of
React itself
, not React components or applications.
The key feature of React is composition of components. Components written by different people should work well together. It is important to us that you can add functionality to a component without causing rippling changes throughout the codebase.
For example, it should be possible to introduce some local state into a component without changing any of the components using it. Similarly, it should be possible to add some initialization and teardown code to any component when necessary.
There is nothing “bad” about using state or lifecycle methods in components. Like any powerful feature, they should be used in moderation, but we have no intention to remove them. On the contrary, we think they are integral parts of what makes React useful. We might enable more functional patterns in the future, but both local state and lifecycle methods will be a part of that model.
Components are often described as “just functions” but in our view they need to be more than that to be useful. In React, components describe any composable behavior, and this includes rendering, lifecycle, and state. Some external libraries like Relay augment components with other responsibilities such as describing data dependencies. It is possible that those ideas might make it back into React too in some form.
Common Abstraction
In general we resist adding features that can be implemented in userland. We don’t want to bloat your apps with useless library code. However, there are exceptions to this.
For example, if React didn’t provide support for local state or lifecycle methods, people would create custom abstractions for them. When there are multiple abstractions competing, React can’t enforce or take advantage of the properties of either of them. It has to work with the lowest common denominator.
This is why sometimes we add features to React itself. If we notice that many components implement a certain feature in incompatible or inefficient ways, we might prefer to bake it into React. We don’t do it lightly. When we do it, it’s because we are confident that raising the abstraction level benefits the whole ecosystem. State, lifecycle methods, cross-browser event normalization are good examples of this.
We always discuss such improvement proposals with the community. You can find some of those discussions by the “big picture” label on the React issue tracker.
Escape Hatches
React is pragmatic. It is driven by the needs of the products written at Facebook. While it is influenced by some paradigms that are not yet fully mainstream such as functional programming, staying accessible to a wide range of developers with different skills and experience levels is an explicit goal of the project.
If we want to deprecate a pattern that we don’t like, it is our responsibility to consider all existing use cases for it and educate the community about the alternatives before we deprecate it. If some pattern that is useful for building apps is hard to express in a declarative way, we will provide an imperative API for it. If we can’t figure out a perfect API for something that we found necessary in many apps, we will provide a temporary subpar working API as long as it is possible to get rid of it later and it leaves the door open for future improvements.
Stability
We value API stability. At Facebook, we have more than 50 thousand components using React. Many other companies, including Twitter and Airbnb, are also heavy users of React. This is why we are usually reluctant to change public APIs or behavior.
However we think stability in the sense of “nothing changes” is overrated. It quickly turns into stagnation. Instead, we prefer the stability in the sense of “It is heavily used in production, and when something changes, there is a clear (and preferably automated) migration path.”
When we deprecate a pattern, we study its internal usage at Facebook and add deprecation warnings. They let us assess the impact of the change. Sometimes we back out if we see that it is too early, and we need to think more strategically about getting the codebases to the point where they are ready for this change.
If we are confident that the change is not too disruptive and the migration strategy is viable for all use cases, we release the deprecation warning to the open source community. We are closely in touch with many users of React outside of Facebook, and we monitor popular open source projects and guide them in fixing those deprecations.
Given the sheer size of the Facebook React codebase, successful internal migration is often a good indicator that other companies won’t have problems either. Nevertheless sometimes people point out additional use cases we haven’t thought of, and we add escape hatches for them or rethink our approach.
We don’t deprecate anything without a good reason. We recognize that sometimes deprecations warnings cause frustration but we add them because deprecations clean up the road for the improvements and new features that we and many people in the community consider valuable.
For example, we added a warning about unknown DOM props in React 15.2.0. Many projects were affected by this. However fixing this warning is important so that we can introduce the support for custom attributes to React. There is a reason like this behind every deprecation that we add.
When we add a deprecation warning, we keep it for the rest of the current major version, and change the behavior in the next major version. If there is a lot of repetitive manual work involved, we release a codemod script that automates most of the change. Codemods enable us to move forward without stagnation in a massive codebase, and we encourage you to use them as well.
You can find the codemods that we released in the react-codemod repository.
Interoperability
We place high value in interoperability with existing systems and gradual adoption. Facebook has a massive non-React codebase. Its website uses a mix of a server-side component system called XHP, internal UI libraries that came before React, and React itself. It is important to us that any product team can start using React for a small feature rather than rewrite their code to bet on it.
This is why React provides escape hatches to work with mutable models, and tries to work well together with other UI libraries. You can wrap an existing imperative UI into a declarative component, and vice versa. This is crucial for gradual adoption.
Scheduling
Even when your components are described as functions, when you use React you don’t call them directly. Every component returns a description of what needs to be rendered, and that description may include both user-written components like
<LikeButton>
and platform-specific components like
<div>
. It is up to React to “unroll”
<LikeButton>
at some point in the future and actually apply changes to the UI tree according to the render results of the components recursively.
This is a subtle distinction but a powerful one. Since you don’t call that component function but let React call it, it means React has the power to delay calling it if necessary. In its current implementation React walks the tree recursively and calls render functions of the whole updated tree during a single tick. However in the future it might start delaying some updates to avoid dropping frames.
This is a common theme in React design. Some popular libraries implement the “push” approach where computations are performed when the new data is available. React, however, sticks to the “pull” approach where computations can be delayed until necessary.
React is not a generic data processing library. It is a library for building user interfaces. We think that it is uniquely positioned in an app to know which computations are relevant right now and which are not.
If something is offscreen, we can delay any logic related to it. If data is arriving faster than the frame rate, we can coalesce and batch updates. We can prioritize work coming from user interactions (such as an animation caused by a button click) over less important background work (such as rendering new content just loaded from the network) to avoid dropping frames.
To be clear, we are not taking advantage of this right now. However the freedom to do something like this is why we prefer to have control over scheduling, and why
setState()
is asynchronous. Conceptually, we think of it as “scheduling an update”.
The control over scheduling would be harder for us to gain if we let the user directly compose views with a “push” based paradigm common in some variations of Functional Reactive Programming. We want to own the “glue” code.
It is a key goal for React that the amount of the user code that executes before yielding back into React is minimal. This ensures that React retains the capability to schedule and split work in chunks according to what it knows about the UI.
There is an internal joke in the team that React should have been called “Schedule” because React does not want to be fully “reactive”.
Developer Experience
Providing a good developer experience is important to us.
For example, we maintain React DevTools which let you inspect the React component tree in Chrome and Firefox. We have heard that it brings a big productivity boost both to the Facebook engineers and to the community.
We also try to go an extra mile to provide helpful developer warnings. For example, React warns you in development if you nest tags in a way that the browser doesn’t understand, or if you make a common typo in the API. Developer warnings and the related checks are the main reason why the development version of React is slower than the production version.
The usage patterns that we see internally at Facebook help us understand what the common mistakes are, and how to prevent them early. When we add new features, we try to anticipate the common mistakes and warn about them.
We are always looking out for ways to improve the developer experience. We love to hear your suggestions and accept your contributions to make it even better.
Debugging
When something goes wrong, it is important that you have breadcrumbs to trace the mistake to its source in the codebase. In React, props and state are those breadcrumbs.
If you see something wrong on the screen, you can open React DevTools, find the component responsible for rendering, and then see if the props and state are correct. If they are, you know that the problem is in the component’s
render()
function, or some function that is called by
render()
. The problem is isolated.
If the state is wrong, you know that the problem is caused by one of the
setState()
calls in this file. This, too, is relatively simple to locate and fix because usually there are only a few
setState()
calls in a single file.
If the props are wrong, you can traverse the tree up in the inspector, looking for the component that first “poisoned the well” by passing bad props down.
This ability to trace any UI to the data that produced it in the form of current props and state is very important to React. It is an explicit design goal that state is not “trapped” in closures and combinators, and is available to React directly.
While the UI is dynamic, we believe that synchronous
render()
functions of props and state turn debugging from guesswork into a boring but finite procedure. We would like to preserve this constraint in React even though it makes some use cases, like complex animations, harder.
Configuration
We find global runtime configuration options to be problematic.
For example, it is occasionally requested that we implement a function like
React.configure(options)
or
React.register(component)
. However this poses multiple problems, and we are not aware of good solutions to them.
What if somebody calls such a function from a third-party component library? What if one React app embeds another React app, and their desired configurations are incompatible? How can a third-party component specify that it requires a particular configuration? We think that global configuration doesn’t work well with composition. Since composition is central to React, we don’t provide global configuration in code.
We do, however, provide some global configuration on the build level. For example, we provide separate development and production builds. We may also add a profiling build in the future, and we are open to considering other build flags.
Beyond the DOM
We see the value of React in the way it allows us to write components that have fewer bugs and compose together well. DOM is the original rendering target for React but React Native is just as important both to Facebook and the community.
Being renderer-agnostic is an important design constraint of React. It adds some overhead in the internal representations. On the other hand, any improvements to the core translate across platforms.
Having a single programming model lets us form engineering teams around products instead of platforms. So far the tradeoff has been worth it for us.
Implementation
We try to provide elegant APIs where possible. We are much less concerned with the implementation being elegant. The real world is far from perfect, and to a reasonable extent we prefer to put the ugly code into the library if it means the user does not have to write it. When we evaluate new code, we are looking for an implementation that is correct, performant and affords a good developer experience. Elegance is secondary.
We prefer boring code to clever code. Code is disposable and often changes. So it is important that it doesn’t introduce new internal abstractions unless absolutely necessary. Verbose code that is easy to move around, change and remove is preferred to elegant code that is prematurely abstracted and hard to change.
Optimized for Tooling
Some commonly used APIs have verbose names. For example, we use
componentDidMount()
instead of
didMount()
or
onMount()
. This is intentional. The goal is to make the points of interaction with the library highly visible.
In a massive codebase like Facebook, being able to search for uses of specific APIs is very important. We value distinct verbose names, and especially for the features that should be used sparingly. For example,
dangerouslySetInnerHTML
is hard to miss in a code review.
Optimizing for search is also important because of our reliance on codemods to make breaking changes. We want it to be easy and safe to apply vast automated changes across the codebase, and unique verbose names help us achieve this. Similarly, distinctive names make it easy to write custom lint rules about using React without worrying about potential false positives.
JSX plays a similar role. While it is not required with React, we use it extensively at Facebook both for aesthetic and pragmatic reasons.
In our codebase, JSX provides an unambiguous hint to the tools that they are dealing with a React element tree. This makes it possible to add build-time optimizations such as hoisting constant elements, safely lint and codemod internal component usage, and include JSX source location into the warnings.
Dogfooding
We try our best to address the problems raised by the community. However we are likely to prioritize the issues that people are
also
experiencing internally at Facebook. Perhaps counter-intuitively, we think this is the main reason why the community can bet on React.
Heavy internal usage gives us the confidence that React won’t disappear tomorrow. React was created at Facebook to solve its problems. It brings tangible business value to the company and is used in many of its products. Dogfooding it means that our vision stays sharp and we have a focused direction going forward.
This doesn’t mean that we ignore the issues raised by the community. For example, we added support for web components and SVG to React even though we don’t rely on either of them internally. We are actively listening to your pain points and address them to the best of our ability. The community is what makes React special to us, and we are honored to contribute back.
After releasing many open source projects at Facebook, we have learned that trying to make everyone happy at the same time produced projects with poor focus that didn’t grow well. Instead, we found that picking a small audience and focusing on making them happy brings a positive net effect. That’s exactly what we did with React, and so far solving the problems encountered by Facebook product teams has translated well to the open source community.
The downside of this approach is that sometimes we fail to give enough focus to the things that Facebook teams don’t have to deal with, such as the “getting started” experience. We are acutely aware of this, and we are thinking of how to improve in a way that would benefit everyone in the community without making the same mistakes we did with open source projects before.