React JS Best Practices From The New Docs
If you don’t have time to read the new React docs, I sifted through them for you.
Last Updated: 2024–08–30
New React docs were recently released on the official React website. It is the best way to learn the framework.
React veterans out there know that to be a true React grandmaster, you need to follow every React team member on Twitter to make sure you don’t miss any precious nuggets of information.
Not anymore! All that esoteric knowledge is now available in an easy-to-read format. Ferocious kids coming from behind will take your jobs thanks to the new React docs!
Your only chance to stay ahead is to humble yourself, sit down, and read every single word in the new docs.
But fear not, for I have selflessly taken one for the team. I went through the docs from top to bottom and broke it all down for you.
Disclaimer: This article contains cursing. Think of me as an unruly React sensei who’s been through the trenches and is now dispensing some wise hot takes and giving zero fucks.
Introduction
The new React docs have two sections: “Learn” and “API Reference.”
This article covers the “Learn” section. It’s meant to be read sequentially.
If you are an experienced React dev, you can skip ahead and skim through the headlines. If something piques your interest, feel free to deep dive.
Section 1: About React
1. React was originally created by Jordan Walke.
Today, Jordan Walke doesn’t work at Meta anymore.
2. React has 21 core team members.
Compare with Vue’s 20 team members and Solid’s 6 team members. Svelte doesn’t publish the number of core team members.
Section 2: Getting Started
3. The docs use CodeSandbox for their interactive code examples.
The docs also mention that React is supported by several other sandbox services. They explicitly name CodeSandbox, StackBlitz, and CodePen.
4. The new docs take a page from Vue on explaining the different use cases of React.
From “adding some interactivity” to “more complex apps” and even “large apps”, the new docs do a great job at mapping out the different levels of React usage.
5. Adding React to a simple HTML page is easy. It doesn’t even require a build step.
Just add React as a script tag, write a component, and render it on a root.
If you want to add JSX support, you can set up a simple build step with babel-cli.
If you want to add JSX support without a build step, make sure to use the HTML starter file provided by the React docs, which includes a standalone Babel script for JSX. Note that this is very slow, so it should only be used for testing or demos.
6. You can have multiple React roots.
Again, this is useful if you want to add a few interactive components to a static HTML page. You can simply create a root for each component, instead of creating a root for the full document. Yet, the docs don’t clarify if it’s possible to share information between components on different roots.
7. The new docs no longer recommend the godawful Create React App.
Create React App was convenient, but it’s a pile of outdated trash that never fixed important bugs. Just take a look at the Github issues if you don’t believe me. The docs now recommend popular alternatives like Vite and Parcel.
8. Next.js is recommended for production-ready projects.
No surprises here. Next.js is good, especially now that it allows more flexibility on page layouts. Other recommendations are Gatsby, Remix, and Razzle.
9. There’s a fine list of tool recommendations to build your own toolchain.
If you want to fly solo, the docs suggest a sensible and trendy list of options. A pleasing surprise to me was that there’s no mention of Lerna, which makes sense after recent debacles. Instead, the recommendations for monorepos are Nx and Turborepo.
10. The recommended editors are VS Code, WebStorm, Sublime Text, and Vim.
No big surprises there. I would only add that any JetBrains editor is up to the task, not only WebStorm. Also, Sublime Text is a meme, Dan Abramov only mentioned it because he still feels bad about that time on a conference talk when the Sublime Text pop-up for buying a licence appeared on screen (he had the licence btw, it was just a technical issue).
Also, Vim’s website looks straight out of the 90s, making the editor literally unusable. It will probably be replaced by Neovim in the docs in a few years.
11. ESLint is required to properly use React.
As it stands, any sane use of React requires the eslint-plugin-react-hooks to be running at all times. In the future this might be implemented on Rome, a compiler step, or some other tool.
12. Prettier is king.
No surprises here. The docs encourage you to use prettier or else, and that’s a good thing. If your codebase uses both ESLint and prettier, the preset eslint-config-prettier is recommended so that ESLint is only used for catching logical mistakes, letting daddy Prettier do all the important work.
13. There’s no React Developer Tools browser extension for Safari.
There’s one for Chrome, Firefox and Edge.
To debug on Safari, there’s a workaround involving the standalone react-devtools npm package, which can also be used to test React Native apps.
14. React apps are made out of components.
The docs blatantly tell you that the main abstraction of the framework is components. What can I say about the mighty MVC killer that hasn’t been said before?
They explicitly say that “A component can be as small as a button, or as large as an entire page,” and that it’s “components all the way down.” Isn’t that beautiful?
15. To learn about JavaScript, the docs recommend MDN and javascript.info.
I was not aware of javascript.info, but it’s open-source and looks pretty legit.
16. They recommend an online tool to convert HTML to JSX
This can be useful if you start from a lot of HTML. It’s needed because JSX is stricter than HTML. For example, JSX requires closing tags. Here’s the tool.
That tool is a goldmine by the way, as it includes SVG to JSX, CSS to Tailwind, and much more.
17. React does not prescribe how to add CSS.
In the simplest case, you’ll add a <link> tag to your HTML. If you use a build tool or a framework, consult its documentation to learn how to add style to your project.
18. You can think of curly braces in JSX as an “escape hatch” into JavaScript.
It’s a nice way to think about them. You can put curly braces in the value of JSX attributes, or inside the JSX tag content.
Curly braces are “a window into the JavaScript world.”
19. The ? and && operators are recommended if you want conditional rendering inside JSX.
It’s nice to see that a tradition as old as time is still recommended. The amount of perverse satisfaction you get from using those operators is unrivalled.
20. The “key” attribute is clearly explained.
An often confusing term, key, is explained pretty well on the new docs. It’s so good that I’m gonna copy-paste the entire section here:
For each item in a list [of components], you should pass a string or a number that uniquely identifies that item among its siblings. Usually, a key should be coming from your data, such as a database ID. React will rely on your keys to understand what happened if you later insert, delete, or reorder the items.
As we’ll see later, “key” is also used for the advanced case of resetting a component’s state. But I still consider the explanation complete because that advanced case is just a consequence of the aforementioned “uniqueness”.
And remember: Keys must not change, or that defeats their purpose! Don’t generate them while rendering. Instead, use a stable ID based on the data.
21. The docs are very good at explaining what’s a rule and what’s a convention.
For example, they explain that useState returns two things, and it’s only by convention that we call them [something, setSomething].
As another example, they explain that it is common to name event handlers as “handle” followed by the event name. (onClick={handleClick}
, onMouseEnter={handleMouseEnter}
, and so on.)
In contrast, Hooks are functions that, as a rule, must start with “use”.
22. “If you want to use hooks in a condition or a loop, extract a new component and put it there.”
You heard it here first.
The new React docs go hard on the fact that hooks are top-level only. If you feel inclined to put a hook inside a condition or loop, that means that you need to create a new component.
Hooks are functions, but it’s helpful to think of them as unconditional declarations about your component’s needs. You “use” React features at the top of your component similar to how you “import” modules at the top of your file.
23. “Lifting state up” is front and center in the docs.
The famous refactoring pattern is so common that it’s explained in full detail on the “Quick Start” page. This should probably keep those peskier devs in check.
24. Turning a UI mockup into a component hierarchy is a creative process.
The docs don’t shy away from the fact that, given a UI mockup, there’s no one right way to turn it into a set of React components.
However, they encourage some common-sense guidelines, like “a component should ideally only do one thing. If it ends up growing, it should be decomposed into smaller subcomponents.”
25. It’s ok to be top or bottom.
When making a component hierarchy, the docs mention optional techniques like “building a static version first,” and they explore the distinction between building “top down” by starting with building the components higher up in the hierarchy or “bottom up” by working from components lower down.
26. Minimal state is cool.
The docs really emphasize the DRY (Don’t Repeat Yourself) principle when it comes to state. In particular, there’s no need to create new state when it can be computed from existing state.
Looks like the React team have been in contact with the same kind of data hoarding that I have seen in other developers.
27. Props vs State is explained clearly.
“Props are like arguments you pass to a function,” while “State is like a component’s memory.”
28. It’s ok to create a new component solely for holding shared state.
If you can’t find a component where it makes sense to own the state that’s used by several children, it’s ok to create a new one.
29. Hooks are called hooks because they let you “hook into” a component’s render cycle.
It’s always good to know what you are hooking into. Now you know.
30. In a sense, React actually uses “two-way data binding.”
Two-way data binding was the revolutionary solution of the currently-pagan framework Angular. Purist might be surprised to know that React does pretty much the same thing because it uses both “one-way data flow” and “inverse data flow.”
“One-way data flow” is the passing of data from the top to the bottom of the component hierarchy. “Inverse data flow” happens when a component deep in the hierarchy needs to update state at the top (usually because of user input.)
The catch, and the reason why React is not technically “two-way data binding,” is because React is very explicit about “inverse data flow”: The developer actually needs to write onChange event handlers.
Section 3: Describing the UI
31. The docs recommend the component libraries Chakra UI and Material UI.
Yes, in that order. Chakra UI is recommended before Material UI.
As an extra offense, Material UI is not even the current name. These days it goes by MUI as an attempt to distance itself from being a library tied to a specific artistic movement.
32. React has a philosophy of “interactivity first.”
Purists have recently claimed that the web is first and foremost a document sharing platform, and that interactivity should be secondary.
The React team strongly disagrees, and they make it clear on the new docs. I’ll just copy-paste what they have to say about it because it’s delightfully savage:
Traditionally when creating web pages, web developers marked up their content and then added interaction by sprinkling on some JavaScript. This worked great when interaction was a nice-to-have on the web. Now it is expected for many sites and all apps. React puts interactivity first while still using the same technology: a React component is a JavaScript function that you can sprinkle with markup.
Linux boomers are recommended to apply aloe to the burn.
To be fair, the docs make it clear that React can also be used to “add sprinkles of interactivity.”
33. In React, rendering logic and markup live together.
Another guiding principle stated in the docs is that logic and markup should be together (meaning, on the same file). This is good because it “ensures that they stay in sync with each other on every edit.”
Author’s note: Maybe Styled Components and Tailwind got so popular because, in a similar way, they allow style to also live together with markup and logic in the same file.
34. React takes no side on the “default vs named exports” debate.
To their credit, they succinctly explain the downside of default exports, saying that “you could write import Banana from './button.js'
and it would still provide you with the same default export.”
Yet, their final say on the matter is just the observation that “People often use default exports if the file exports only one component, and use named exports if it exports multiple components and values.”
They also don’t shy away from having “one default export and numerous named exports.”
35. Never define a component inside another component!
According to the docs, it’s technically possible. But it’s very slow and causes bugs. Instead, define every component at the top level.
36. It’s ok to make components even if they are not reusable.
React is “components all the way down,” so there will always be at least one non-reausable component: the top-level “app component.”
But it’s ok to have even more one-off components because “components are a handy way to organize UI code and markup, even if some of them are only used once.”
37. Fragments let you return more than one element.
The docs explain why a component can’t return more than one element: It’s because “JSX under the hood is transformed into plain JavaScript objects. You can’t return two objects from a function without wrapping them into an array.”
But fear not, for you can return two JSX tags by “wrapping them into another tag or a Fragment.”
Fragments are “empty tags” ( <>…</>
) that let you group things without leaving any trace in the browser HTML tree.
Another benefit of Fragments is that they let you pass a key (only possible when using the more explicit <Fragment>
syntax), so they’re great when an item in a list of components needs to render several elements.
38. aria-* and data-* are the only JSX attributes written with dashes.
This is for historical reasons.
In general, everything in JSX is camelCase, even inline style properties, because JavaScript has limitations on variable names.
Additionally, since class
is a reserved word, in React you write className
instead.
39. “Double Curlies” is an official term.
It’s the explicit name for passing a JS object in JSX, in which you must wrap the object in another pair of curly braces: {{}}.
It’s not a special syntax, it’s just a JavaScript object tucked inside JSX curly braces.
Not to be confused with the “vagina syntax” when returning an object from an arrow function: ({}).
40. The props you can pass to HTML tags are predefined.
For example, “className”, “src”, “alt”, “width”, and “height” are some of the props you can pass to an <img> tag.
But you can pass any props to your own components.
41. If you are spreading all the time, you might be behaving immorally.
Some components forward all of their props to their children. As they don’t use any of their props directly, it can make sense to use the concise “spread” syntax: {...props}
However, the docs warn to “Use spread syntax with restraint. If you’re using it in every other component, something is wrong. Often, it indicates that you should split your components.”
Careful who you spread for.
42. Some components have a “hole” that can be “filled in.”
You can think of a component with a children
prop as having a “hole” that can be “filled in” by its parent components with arbitrary JSX.
You will often use the children
prop for visual wrappers: panels, grids, and so on.
43. A component may receive different props over time.
A superficial understanding of the difference between state and props might make you think that props don’t change.
Not so! A parent component might have state that changes over time and passes it down to a child component as props.
However, from the point of view of the child component, props are “immutable” — a term from computer science meaning “unchangeable”
So, when a child component needs to change its props (for example, in response to a user interaction or new data) it must “ask” the parent component to generate and pass “new props” by using the aforementioned “inverse data flow.”
We can say that props are read-only snapshots in time: every render receives a new version of props. (And, as we’ll see later, state is also a read-only snapshot in time)
44. The docs recommend to use ternary operators “in moderation.”
They are ok to conditionally render within JSX, but “If your components get messy with too much nested conditional markup, consider extracting child components to clean things up.”
If fellow devs insist on creating a spaghetti sauce of ternary operators, consider sending them to the React docs, or here:
45. Don’t put numbers on the left side of &&.
Old timers know this one.
If you use && for conditional rendering, you’re gonna have a bad time with 0. You will likely end up with a small unstyled zero new to your shinny sans-serif material UI.
Mathematicians hate it.
46. Sometimes it’s best to avoid shortcuts and just conditionally assign JSX to a variable.
Ternary operators and &&s are nice, but if they get in the way, you might as well use old school variables to hold the conditional JSX.
I’m glad that the docs plainly say that it’s ok to write normal JavaScript. Sugar is good, but vanilla JS is sweet enough.
47. The docs recommend “crypto.randomUUID()” or a package like “uuid.”
They can help you create unique identifiers for data generated and persisted locally, which you can then use as keys.
Yes, they recommend crypto.randomUUID() before the “uuid” package.
I never heard of that native crypto library before, but it looks solid. If I were the maintainer of the “uuid” package, I would start looking for a new job. These docs just ended their entire career.
48. Your components won’t receive key
as a prop.
It’s only used as a hint by React itself.
If your component needs the ID that was used as the key, you have to pass it as a separate prop too.
49. React assumes that every component you write is a pure function.
Don’t go around doing impure things behind React’s back. React is designed around the concept of pure functions.
A pure function has the following characteristics:
- It minds its own business. It does not change any objects or variables that existed before it was called. (This means that it doesn’t mutate variables outside of the function’s scope, and it doesn’t mutate it’s inputs. In React, “props”, “state” and “context” are all considered inputs.)
- Same inputs, same output. Given the same inputs, a pure function should always return the same result.
However, it’s completely fine to change variables and objects that you’ve just created inside the function. This is fine because no code outside of the function will ever know that this happened. This is called “local mutation.”
React cares about purity because it helps with performance, server-rendering, and it will enable cool new features soon™️.
Note: For the only exception to this rule, check (99).
50. Detecting impure calculations with StrictMode.
If you are in strict mode, every component will be rendered twice. This is to detect if you are a naughty boy.
51. Not everything is pure on React ;)
I’ll let you in on a little secret, all of us React devs do impure things all the time. And we love it!
While functional programming relies heavily on purity, at some point, somewhere, something has to change. That’s kind of the point of programming!
So, we have our little secret spots on React where we can do whatever we want. We do things differently “on the side.” Tee-hee-hee!
In React, side effects usually belong inside “event handlers,” from which you are free to modify external variables, set state, and do whatever impure thing you desire. Even though event handlers are defined inside your component, they don’t run during rendering, so they don’t need to be pure!
If you’ve exhausted all other options and can’t find the right event handler for your side effect, you can still attach it to your returned JSX with a “useEffect” call. This tells React to execute it later, after rendering, when side effects are allowed.
Section 4: Adding Interactivity
52. They link to a Medium article about design systems.
They really do. Here it is:
Congrats to Audrey Hacq, you really made it into the React docs. Can’t wait to see the movie adaptation of your article.
53. They say that event objects are usually called “e” by convention.
Bad move, React Docs, bad move.
I’ve been fighting devs for years to write those as “event.”
What am I gonna do now, React? Tell me, what the fuck am I supposed to do with all those “e”s on my otherwise pristine codebase?
54. Event handlers are just functions that you pass as props.
There’s nothing special about them.
Except of course that they are meant to respond to interactions like clicking, hovering, and so on.
Which means that every event handler, at the very bottom of the React component tree, will land on one of the built-in handlers like “onClick” on some HTML tag like “button”.
55. You should name event handlers based on app-specific concepts.
When your component supports multiple interactions, you might name event handler props for app-specific concepts. For example “onPlayMovie” and “onUploadImage”.
56. The docs remind you that capture and propagation exist.
If you were lucky enough to work on high-level stuff and rely solely on event handlers, the React docs are there to remind you that the complete hell of DOM event propagation is still alive. May God help us all.
57. e.stopPropagation() stops propagation.
It does. It prevents an event from reaching parent components.
58. e.preventDefault() prevents default.
How nice. This prevents default browser behavior on some events. For exemple, browsers reload the entire page by default on the form submit event.
59. Local variables don’t persist between renders.
When React renders a component a second time, it renders it from scratch — it doesn’t consider any changes to local variables.
By “local variables” I mean something like let index = 0
on the body of the component.
60. Changes to local variables won’t trigger renders.
Few things trigger renders. Changing local variables ain’t one of them. Did you thought this is Svelte?
61. To update a component with new data, two things need to happen.
First, we need to persist the new data between renders. Second, we need to trigger React to render the component with the new data (re-rendering).
Here’s where the useState Hook comes in. It provides those two things:
- A state variable to retain data between renders.
- A state setter function (aka “set function” or “set state function”) to update the variable and trigger React to render the component again.
Could this be done in a simpler way? Sure, but this isn’t Svelte. This is React, we live close to the metal and we like it like that. No magic on my JavaScript.
62. State is private to each component instance on the screen.
State is not tied to a particular component function. If you render the same component in two places on the screen, each copy gets its own state.
This might seem obvious to veterans but it can trip you up the first times using React. Especially when using useState, which is something that you have to import, and importing is mysterious because who knows what’s on the other side.
63. Triggering, rendering, and committing.
In React, the process of updating a component on the screen has three steps:
Triggering a render is queueing React to render a component as soon as it’s not too busy doing something else, like wasting CPU cycles.
Rendering is running your component and obtaining the result.
Committing is getting down and dirty and actually applying the result of rendering into the infernal machine of the browser DOM API, using contraptions like the Document.createElement() method.
63. There are two reasons for a component to render.
- It’s the component’s initial render.
- The component’s (or one of its ancestors’) state has been updated.
That’s it.
But how about when a context changes? Nope, that case is already contained within “one of its ancestors’ state has been updated.”
64. There are ways to increase performance, but thread carefully.
The default behavior of rendering all components nested within the updated component is not optimal for performance if the updated component is very high in the tree.
If you run into a performance issue, there are several opt-in ways to solve it, but don’t optimize prematurely!
The main optimization technique recommended in the docs is the “memo” API. It allows you to skip rendering components whose inputs have not changed. This is safe because pure functions always return the same results, so they are safe to cache.
See? This is why all that purity nonsense is not a waste of time!
65. React is smart. Maybe smarter than you!
For re-renders, React will apply the minimal necessary operations (calculated while rendering!) to make the DOM match the latest rendering output.
This right here is React’s claim to fame. Initially called the Virtual DOM or “the diff,” nowadays — especially after some vicious attacks — it’s just referred as “that thing React does,” and no questions are asked because the implementation is so obscure and arcane that you might as well apply to join the Illuminati.
66. React only changes the DOM nodes if there’s a difference between renders.
This might be one of the coolest thing ever.
Let me be clear here: Even if a component re-renders because something changed, React will only commit changes for those specific DOM elements that changed!
Every unaffected DOM element within the re-rendered component will be let at peace! Jesus Christ! Who ever thought of that? Those React folks really had a big brain moment!
67. State is like a snapshot of the UI in time.
State might look like regular JavaScript variables that you can read and write to. However, state behaves more like a snapshot. Setting it does not change the state variable you already have, but instead triggers a re-render.
This is because “rendering” is actually taking a snapshot of the UI in time. When React calls your component function, it generates that snapshot. Its props, state, event handlers, Effects, and local variables are all calculated using its state at the time of the render.
Unlike a photograph or a movie frame, the UI “snapshot” you return is interactive. It includes logic like event handlers that specify what happens in response to inputs.
So here’s the catch: You can’t change state from an event handler and then expect to read the updated value from the same event handler, because you are still trapped on the same snapshot in time. Setting state only changes it for the next render.
68. A state variable’s value never changes within a render.
Just remember this maxim: “A state variable’s value never changes within a render.”
Whatever you do, even if you wrap a console.log into three setTimeouts, you will never escape your render snapshot. The updated state is a privilege reserved for future renders. Abandon all hope.
69. React batches state updates.
React waits until all code in your event handler has run before processing your state updates. This is useful if your event handler sets multiple states, or changes the same state multiple times.
This behavior, known as batching, makes your app run much faster because it avoids unnecessary re-renders. It also avoids dealing with confusing “half-finished” renders where only some of the variables have been updated.
Let’s look at an example event handler:
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
}
Instead of rendering twice, once for each state update, React will only re-render once at the end.
70. For async even handlers, the “async” part might run on a future batch.
We just said that React waits until all code in your event handlers has run before processing your state updates. But there’s an exception:
When batching async even handlers, React doesn’t wait for the “async” part, it just waits for the event handler itself. This means that, if an event handler sets state after timeouts, promises, or awaits, the deferred state updates might run on a future batch. But, as event handlers are snapshots in time, the future batch will also be tied to the state as it existed at the time of the render.
Let’s look at an example:
async function handleClick() {
setPending(pending + 1); // batch 1
setYolo(true); // batch 1
await delay(3000);
setPending(pending - 1); // batch 2
setCompleted(completed + 1); // batch 2
}
Here, the state updates before the “await” will run on one batch, and the state updates after the “await” will run on a future batch.
Before React 18, the async part was not batched at all. For historical information about those dark times, check this.
71. “Updater functions” allow you to set the same state multiple times from an event handler.
It is an uncommon use case, but if you would like to update the same state multiple times before the next render, instead of passing the next value, you can pass an updater function that calculates the next state based on the previous one in the queue, like setNumber(n => n + 1)
.
It is a way to tell React to “do something with the state value” instead of just replacing it with some specific value.
You can set state with an updater function multiple times and they will all be queued into the next batch update.
Author’s note: It’s funny to see “updater functions” downplayed as an “uncommon use case” in the new docs. Before, they were the go-to way to update state if you wanted to avoid rare batching bugs. But I guess that’s not the case anymore.
72. Keep “updater functions” pure.
I don’t even want to know what sort of degenerate developers are out there. But know this, if you do impure things on updater functions, StrictMode will catch you.
73. The docs recommend a naming convention for updater functions.
The docs recommend to name the updater function argument by the first letters of the corresponding state variable:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
It is a bit of a bizarre recommendation, as I always use “value” for the state value. Whatever, React docs, you do you.
74. Batched state updates are internally implemented as a simple queue.
I like how the new docs explain how some React internals are implemented.
The internal queue used by React to batch state updates is so simple that any dev could recreate it. And it’s a good way to ossify React knowledge deep inside your amygdala.
The internal queue is just an array containing the things that were batched, which can be:
- Values from regular updates, or
- “updater functions.”
Here’s how it works: The queue gets iterated. If a value is found, it replaces the state. If a function is found, it is run on the current state value. Easy.
75. Treat state as read-only.
It should come as no surprise — considering that state is a snapshot in time and that React is obsessed with functional programming — that everything you put on React state should be treated as read-only and “immutable.”
This is an example of what you should never do:
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com'
});
function handleFirstNameChange(e) {
person.firstName = e.target.value; // <-- never do this
}
What are you doing? Don’t you see the ‘setPerson’ function sitting all alone up there?
There are two problems with that code:
- It doesn’t trigger a state update because it doesn’t call the set function.
- Objects are mutable in JavaScript, so what the code is actually doing is changing the state in the previous render “snapshot”. It’s like trying to change the order after you’ve already eaten the meal. Also, it messes up with debuggability and with React’s internal optimizations.
Try this instead:
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com'
});
function handleFirstNameChange(e) {
setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});
}
That is, you should create a new object and pass it to the state setting function.
76. You can create new objects with the spread syntax.
As seen above, the docs recommend the good ol’ spread syntax (…) to create the new object based on the existing one in state.
setPerson({
...person, // Copy the old fields
firstName: e.target.value // But override this one
});
77. Be careful with nested objects in state.
Careful though, spread is “shallow” — it only copies things one level deep.
If you are vicious and want nested objects in state, you have to spread more and create new objects for every level of nesting.
78. Objects are not “really” nested in JavaScript.
With all this talk of spreading and nesting, the docs are wise to remind us that objects are not really nested inside each other, they just reference each other.
In fact, there could be all sorts of circular and psychedelic cross-referencing that would completely break the nesting metaphor. One can only watch in despair as all the structures of our western civilization collapse!
79. The docs recommend Immer if you don’t like too much spreading.
For deeply nested state, the docs recommend Immer, in which your updates look like you are “breaking all the rules” and mutating state:
const [person, updatePerson] = useImmer({
name: "Michel",
age: 33
});
updatePerson(draft => {
draft.name = 'Sebastian';
});
But Immer, thanks to the magic of JavaScript Proxies, creates a new object in the background.
The React docs even suggest the React-specific use-immer
wrapper, in which you import useImmer
instead of useState
.
80. Treat arrays in state as read-only too.
Just like with objects, when you want to update an array stored in state, you need to create a new one (or make a copy of an existing one).
81. The docs provide a table of which methods to avoid for arrays in state.
Here it is:
Naturally, if you use Immer, all methods are available.
Personally, I never use “concat”. I like to spread.
I don’t like to use “slice” because I always confuse it with it’s evil twin “splice.” But sometimes, when you want to insert into an array in a specific position, it’s the only way. Luckily, Github Copilot can save me a trip to StackOverflow when that situation arises.
82. The docs remind you that arrays can contain objects.
Sometimes, creating new arrays is not enough. If they contain objects, you should take care to create new objects too when updating state.
Again, Immer could be of help, especially if you have very nested data structures.
Section 5: Managing State
83. Think like a designer when managing your state.
The docs are happy to remind you that we don’t live in the jQuery dark ages anymore. You are driving a declarative framework, not an imperative monstrosity.
Therefore, you are well advised to think higher-level. Instead of focusing on minuscule behaviours of buttons and divs, you should think of the different states that your whole component can have, such as “loading,” “initial,” “error,” “success.” And which actions trigger transitions between them.
Keep in mind “state machines” and “designer mockups” when defining your state, and React will leverage it to keep the complexity low.
If this sounds a bit like magical thinking, it is. But it’s magical thinking by some of the most successful UX programmers of our time, so shut up and listen.
84. Declarative logic might use more lines of code than imperative logic.
Despite what we said above, thinking like a designer might produce more lines of code. And there’s an example of this in the docs.
Still, this is a worthwhile tradeoff because the declarative code should be less fragile and it should let you introduce new states without breaking old ones.
85. The docs address the usefulness of tools like Storybook.
While the tool Storybook is not explicitly named, the docs say that, if a component has a lot of visual states, it can be convenient to show them all on one page. And these pages are often called “living styleguides” or “storybooks”.
This part of the docs suggests that you don’t necessarily need an external tool to have “storybooks,” they could simply be part of your main app.
86. Group related state.
If some two state variables always change together, it might be a good idea to unify them into a single state variable. Then you won’t forget to always keep them in sync.
87. Avoid contradictions in state.
The docs suggest to design your state so that “impossible” or “contradicting” states can’t happen.
For example, instead of having two states, isSending
and isSent
, which allow for the contradicting case of both being true if some distracted dev forgets to update them correctly, you should have a single status
state with the possible values sending
and sent
.
You can still declare some constants for readability:
const isSending = status === ‘sending’;
const isSent = status === ‘sent’;
But they are not state variables, so they will never be out of sync.
88. Don’t mirror props in state.
Just don’t. Two reasons:
- Why would you even do that?
- If you do it, your state will be out of sync when props change, because state doesn’t update after initial render.
The only legit case of mirroring state is precisely when you want to ignore all updates for a specific prop. In that case, by convention, you should start the prop name with initial
or default
to clarify that its new values are ignored.
89. Avoid deeply nested state.
The docs recommend to have “flatten” or “normalized” state instead of deeply nested state. This is done by having a bunch of ids and arrays of ids, as if pretending to be a database table.
It brings joy to my heart to see that this pattern is still being recommended. It feels like yesterday when Redux-era Dan Abramov taught us uncivilized front-ends how to do things a bit more back-endy.
90. Avoid deeply nested state — extra tip!
Sometimes, you can also reduce state nesting by moving some of the nested state into the child components.
91. “Controlled vs uncontrolled component” is clearly explained.
The myth is put to an end. The description of these terms is so good that I’m just gonna copy-paste from the docs:
It is common to call a component with some local state “uncontrolled”. […]
In contrast, you might say a component is “controlled” when the important information in it is driven by props rather than its own local state. This lets the parent component fully specify its behavior. […]
In practice, “controlled” and “uncontrolled” aren’t strict technical terms — each component usually has some mix of both local state and props. However, this is a useful way to talk about how components are designed and what capabilities they offer.
92. There is a thing called CSSOM.
The docs, in an attempt to prove that browsers use a lot of trees to structure UI, tells us that there’s not only a DOM but a CSSOM (CSS Object Model) too! There’s even an Accessibility tree in the browser too!
93. React has an internal “UI tree”.
It should come as no surprise that React also uses an internal tree to structure the UI:
94. State is tied to a position in the UI tree.
It’s a bit of a simplification to say that “state lives inside the component,” because a component might be rendered in more that one position in the UI tree, each with its own isolated state.
So, more accurately, we can say that state is tied to a component in a specific position in the UI tree.
95. React will keep the state as long as you render the same component at the same position.
The moment you stop rendering a component in a given position (either because it gets removed, or because a different component gets rendered at the same position), its state disappears completely. Actually, the state of its entire subtree disappears completely.
Remember that it’s the position in the UI tree — not in the JSX markup — that matters to React! If your component conditionally returns different JSX tags which both result in the same component in the same position, React will keep the state (Even if the props change!)
Let’s look at an example:
if (isFancy) {
return (
<div className="fancy">
<Counter isFancy={true} />
</div>
)
} else {
return (
<div>
<Counter isFancy={false} />
</div>
)
}
You might expect Counter’s internal state to reset when the condition changes, but it doesn’t!
Here’s a good rule of thumb: If you want to preserve the state between re-renders, the structure of your tree needs to “match up” from one render to another.
96. Resetting state at the same position: The hacky way.
Sometimes you do want a component to reset state. For example, let’s say that you have a Counter component that shows the score for a player:
{isPlayerA ? (
<Counter person="Taylor" />
) : (
<Counter person="Sarah" />
)}
There’s a bug here: When the condition changes, the score remains.
If you want to reset the state when the condition changes, there are two ways. Let’s look at the hacky way first:
{isPlayerA &&
<Counter person="Taylor" />
}
{!isPlayerA &&
<Counter person="Sarah" />
}
This works because null
is a valid element in the UI tree. React recognizes this setup as two independent positions.
Naturally, this is only a good solution if you have a few elements. I was surprised to see this hacky shit in the docs! I don’t recommend it at all.
97. Resetting state at the same position: The Chad way.
React seniors out there know that the legit, generic way to reset state at the same position is to pass a unique key.
You might have seen key
s when rendering lists. You think that’s it? Get ready to get your mind blown.
Keys aren’t just for lists! You can use keys to make React distinguish between any sibling components.
By default, React uses order within the parent (“first counter”, “second counter”) to discern between components. But keys let you tell React that this is not just a first counter, or a second counter, but a specific counter — for example, Taylor’s counter:
{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}
Here, the state will reset when the player changes.
Remember that keys don’t have to be globally unique. They only specify the position within the parent.
98. What if you want to revert the old state when going back?
Do you think that React will do everything for you? Nope!
React gives you the sane default of preserving state for the same component in the same position. And it allows you to opt-out of it by passing a key to mark your component as unique. Now you want more? You want to recover the old state when rendering the component again with the old key?
If you want that, you need to write some actual code and use tried and true patterns like “lifting state up,” or localStorage, or hide stuff with CSS.
99. The Ugliest Pattern In React™️ — Adjusting state in response to rendering.
Brace yourselves. Out of all the nastiness in the React world, nothing comes close to this.
Usually, you will update state in event handlers. However, in rare cases, you might want to adjust state in response to rendering — for example, you might want to change a state variable when a prop changes.
Granted. In most cases, you don’t need this:
- If the value you need can be computed entirely from the current props or other state, then you can remove the redundant state altogether.
- If you want to reset the entire component tree’s state, pass a different key to your component, as we just saw.
- If you can, update all the relevant state in event handlers.
But let’s say that none of those conditions apply, and you still need to update a state when a prop changes.
To solve this problem, seasoned React devs know what to do: Update the state in an Effect.
Nope! The new docs explicitly tell you that an Effect is no good for this because it will cause the child tree to render twice. And sure, you want to avoid extra renders, but at what price?
The new docs recommend instead what I can only call The Ugliest Pattern In React™️. If you are brave to see it for yourself, here it is:
function List({ items }) {
const [selection, setSelection] = useState(null);
// Adjust the state while rendering
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
The horror! Yes, it sets state in the render function! It breaks what’s arguably the most important rule of React: The render function must be pure.
I’ll let the docs excuse themselves and give you the full gory details:
You can only update the state of the currently rendering component like this. Calling the
set
function of another component during rendering is an error. This special case doesn’t mean you can break other rules of pure functions. […]
This pattern can be hard to understand and is usually best avoided. […]The rest of your component function will still execute (and the result will be thrown away), but if your condition is below all the calls to Hooks, you may add an early
return;
inside it to restart rendering earlier.
Yes, they even drive the point home by making me imagine an early return inside of an if-statement that sets state *shivers* in the render function.
The only consolation I can offer is that nothing else in React is uglier than this.
To be fair, this pattern has been around since hooks first released. But it will likely be encouraged a bit more from now on, given the React team’s renewed interest in reducing unwanted uses of Effects.
Maybe the ugliness is a virtue, since it encourages us to completely change the code to enable some of the better alternatives mentioned above — After all, “adjusting state in response to rendering” will always result in code that’s difficult to understand.
Section 6: Reducers
100. Sometimes reducers can help you model complex state.
According to the docs, sometimes a reducer (by “reducer” I mean a useReducer()
reducer) is better than a series of states if you want to faithfully model a complex state machine, in a way that easily avoids “impossible” or “contradicting” states.
Also, your event handlers become concise because they only specify the user “actions.”
101. Each reducer action should describe a single user interaction.
Managing state with reducers is slightly different from directly setting state. The docs recommend that instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers.
Each action should describe a single user interaction, even if that leads to multiple changes in the data. It makes more sense to dispatch one reset_form
action rather than five separate set_field
actions.
The state update logic will live in the reducer.
102. Conventions for reducer action objects.
An action object can have any shape. By convention, it is common to give it a string type
that describes what happened, and pass any additional information in other fields.
dispatch({
type: 'what_happened',
// other fields go here
});
103. Put your reducer outside of your component.
Because the reducer function takes state and actions as arguments, you can declare it outside of your component. This decreases the indentation level and can make your code easier to read.
You can even put it on a different file. Component logic can be easier to read when you separate concerns like this.
104. It’s convention to use “switch” statements in reducers.
Sure, you could also use if-statements, but you would be a bit of a pleb.
If you use “switch” statements, the docs recommend to wrap each case
block into the {
and }
curly braces so that variables declared inside of different case
s don’t clash with each other. Also, a case
should usually end with a return
. If you forget to return
, the code will “fall through” to the next case
.
105. There’s no satisfying explanation to why reducers are called reducers.
The docs give a sad attempt though:
Apparently reducers are a bit like an array’s “reduce” method. But instead of taking “the result so far and the current item,” reducers take “the state so far and the action”.
La di fucking da.
106. Using reducers is a matter of personal preference.
It’s nice for the docs to say that reducers might not be everyone’s cup of tea.
We recommend using a reducer if you often encounter bugs due to incorrect state updates in some component, and want to introduce more structure to its code. You don’t have to use reducers for everything: feel free to mix and match! You can even
useState
anduseReducer
in the same component.
They also say that, in general, reducers are usually easier to debug and test. They are more readable if the logic is complex. But surely they require more code.
This author’s opinion is that reducers are nice, but they are an artifact of the Redux hype of yesteryear, paired with the need for a reliable alternative to useState when hooks were just released, given that there were bugs caused by lack of batching. Lord have mercy!
107. Reducers must be pure.
As almost everything else in React, reducers must be pure. Don’t go around doing any weird shit. As always, you can use Immer to make your existence a little bit less painful.
Section 7: Context
108. Context lets you pass information without “prop drilling.”
I’m happy to say that the new react docs use the phrase “prop drilling,” a phrase already embraced by other frameworks like Vue. Drill my props, daddy!
Passing props is a great way to explicitly pipe data through your UI tree to the components that use it.
But passing props can become verbose and inconvenient when you need to pass some prop deeply through the tree, or if many components need the same prop. The nearest common ancestor could be far removed from the components that need data, and lifting state up that high can lead to a situation sometimes called “prop drilling”.
That’s right, when you need to pipe some data deep, you might as well suck it up and declare a Context. Context lets a parent component provide data to the entire tree below it.
109. Context is as easy as one-two-three!
Using context is easy, just follow these steps:
- Create a context. (With
createContext
.) - Use that context from the component that needs the data. (With
useContext
.) - Provide that context from the component that specifies the data. (By wrapping their children with the context provider
<SomeContext.Provider value={1}>
) (If you don’t provide the context, React will use the context’s default value.)
It saddens me a bit that there’s three elements to handle when working with contexts. In contrast, Vue’s and Svelte’s versions of context requires only two. If you know why React and Solid have an extra step, let me know in the comments!
110. A children using a context will grab from the nearest provider.
That’s right, there can be multiple providers of the same context above you, but when you use a context, you will grab the value from the closest one.
111. You could use and provide the same context from the same component.
Sure, this is indeed some weird shit, but the docs provide a reasonable example.
It could be useful when your nested components want to override the same context for some fucked up reason. I don’t judge.
One component may use or provide many different contexts without a problem.
112. You could think of context as CSS property inheritance.
In CSS, you can specify color: blue
for a <div>
, and any DOM node inside of it, no matter how deep, will inherit that color unless some other DOM node in the middle overrides it with color: green
. Similarly, in React, the only way to override some context coming from above is to wrap children into a context provider with a different value.
Different properties like color
and background-color
don’t override each other. Similarly, different React contexts don’t override each other.
113. Don’t overuse Context!
Stop! Collaborate and listen. The docs warn you about the temptations of context.
Before reaching out for context, keep in mind that a bit of prop drilling is not so bad. It can even make you feel good, as props make the data flow more explicit.
Also, consider that maybe the reason you’re drilling so much is because you haven’t extracted pesky visual components that don’t do anything with the data anyway. Maybe they should just take a children prop instead.
114. The docs list common use cases for context.
- Theming (e.g. dark mode).
- Current logged in user.
- Routing (Most routing solutions use context internally to hold the current route. This is how every link “knows” whether it’s active or not.)
- Managing a lot of state.
115. Remember, Context is not just for static values.
If you pass a different value on the next render, React will update every single component reading it below!
116. Context + Reducer = Chad Developer.
Do you have what it takes to use the Chaddest pattern in all of React? Buckle up, then. We are not in Redux anymore.
With the Context + Reducer pattern, a parent component with complex state manages it with a reducer. Other components anywhere deep in the tree can read its state via context, and they can also dispatch actions to update that state.
117. Even more Chad: Have two contexts for your reducer.
To bring this pattern to the next level, you should have two contexts for your reducer: One for the state, and one for the dispatcher (or for your custom event handlers). Yolo!
Each component reads the context that it needs.
This will prevent unnecessary re-renders because not every component will need both the state and “dispatch”. And “dispatch” never changes, so it will never re-render components that only dispatch.
118. Giga chad: Create custom hooks for your contexts.
Something like this:
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
Now you can use a context without having to import both the context and useContext
. One import is all it takes!
Warning: If any coworker see you typing this, they will know that you are a massive Giga Chad.
Section 8: Refs
119. Think of refs as a “secret pocket.”
Let’s look at how you add a ref to your component:
const ref = useRef(0);
useRef
then returns an object like this:
{
current: 0 // The value you passed to useRef
}
The docs explain refs succinctly with a rich metaphor and a lovely picture that I just had to steal:
A ref is like a “secret pocket” on your component that React doesn’t track.
The ref is a plain JavaScript object that you can’t modify (immutable). In fact, useRef
always returns the same ref object. It has acurrent
property that you can read and modify (mutable).
In the illustration above, the current
property is represented as an arrow because, as an object property, it can “point” to anything that you want to track. (Also, from a nerdier perspective, JavaScript object properties are implemented in the underlying C++ runtime as “pointers”, so the arrow metaphor is very appropriate.)
You can’t change the pocket, and you can’t change the “current” arrow. But you can change where the arrow is pointing to.
120. When to use refs.
Sure, secret pockets are cool, but why would you use them?
Refs are very similar to state. Like state, refs are retained by React between re-renders. Actually, most of the time you want to use state.
But here’s the difference: Setting state re-renders a component. Changing a ref does not!
So, here’s a rule of thumb for when to use refs:
- When a piece of information is used for rendering, keep it in state.
- When a piece of information is not used for rendering (maybe it’s only needed by event handlers or useEffect calls) and changing it doesn’t require a re-render, using a ref may be more efficient.
Typically, you will use a ref when your component needs to “step outside” React and communicate with external APIs — often a browser API that won’t impact the appearance of the component. Common examples of this are:
- Storing timeout IDs (for setTimeout or setInterval)
- Storing DOM elements.
121. You shouldn’t read (or write) the ref.current
value during rendering.
Sure, ref.current
is mutable. But if you are trying to interact with it during rendering, you are doing something wrong.
If some information is needed during rendering, use state instead.
Since React doesn’t know when ref.current
changes, even reading it while rendering makes your component’s behavior difficult to predict.
(The only exception to this is code like if (!ref.current) ref.current = new Thing()
which only sets the ref once during the first render. But this pattern is a bit of a stretch already, since most of the time you would initialize refs on the useRef()
call.)
122. You can combine refs and state in a single component.
Of course you can. Why wouldn’t you?
123. How does useRef work inside?
Get ready to get your mind blown.
The docs explain that, in principle, useRef
could be implemented on top of useState
. You can imagine that inside of React, useRef
is implemented like this:
// Inside of React
function useRef(initialValue) {
const [ref, unused] = useState({ current: initialValue });
return ref;
}
Note how the state setter is unused in this example. It is unnecessary because useRef
always needs to return the same object.
React provides a built-in version of useRef
because it is common enough in practice.
And also because the useState
implementation above only works by breaking the “state is read-only” dogma, which is sacrilegious! React, in its mercy, provides us with useRef
, which is a sanctified way to have mutable state that’s independent of the “one-way data flow.”
124. When you mutate ref.current
, it changes immediately.
Keep in mind that limitations of React state don’t apply to refs. For example, state acts like a snapshot for every render and doesn’t update synchronously. But when you mutate the current value of a ref, it changes immediately:
ref.current = 5;
console.log(ref.current); // 5
This is because the ref itself is a regular JavaScript object, and so it behaves like one.
125. How about using a variable outside of the component instead of refs?
Careful readers might be thinking: “Hold on a minute, refs are mutable data that aren’t used for rendering, so why do I need refs? Why don’t I keep the data in a variable outside of the component?”
You would be right, that’s definitely possible, but that only works if you have only one instance of the component. As soon as you render more instances, they will all share the same external variable, and that might not be what you want.
Using refs works fine for many components because, just like state, refs are tied to a component in a specific position in the UI tree.
126. Refs help you read the latest state from an asynchronous operation.
The docs mention a very common pattern.
State works like a snapshot, so you can’t read the latest state from an asynchronous operation like a timeout. However, you can keep the latest state in a ref! A ref is mutable, so you can read the current
property at any time.
In order to do this, there’s a bit of boilerplate: You need to update the current ref value manually. It’s nice to see that the example in the documentation does this by having two calls in an event handler, instead of the infamous anti-pattern of using useEffect to keep state and ref in sync.
const [text, setText] = useState('');
const textRef = useRef(text);
function handleChange(e) {
setText(e.target.value);
textRef.current = e.target.value;
}
127. React wants you to keep your hands away from the DOM.
Too many cooks will spoil the broth. The entire reason for using React is to let it handle the nasty bits of the DOM.
However, sometimes you might need access to the DOM elements managed by React — for example, to focus a node, scroll to it, or measure its size and position. And to access the DOM elements, you need to use refs.
128. If you really need a DOM node, use a ref.
This is how you do it:
const myRef = useRef(null);
// ...
<div ref={myRef}>
If you perform those steps correctly, React will automagically put a reference to that node into myRef.current
.
Then, you are free to do whatever naughty thing you would do to a DOM node, like myRef.current.scrollIntoView()
. As always, keep dirty side effects like these on event handlers or useEffect calls.
129. How to manage an unknown number of DOM node refs?
In the example above there is a predefined number of refs. However, sometimes you might need a ref to each item in a list, and you don’t know how many you will have.
Then, it’s time to get your hands dirty. There are two ways:
- Get a single ref to their parent element, and then use DOM manipulation methods like
querySelectorAll
. Hacky but legit! - A more React-y solution involves passing a function to the ref attribute, which is called a “ref callback”. In short, you need to manually code the magic that React does for you in the previous case. Full details here.
130. Use ‘forwardRef’ to access another component’s DOM nodes.
So far we dealt with ref attributes on built-in components for DOM nodes. But if you try to put a ref on your own component, the magic is gone, by default you will get null
.
What were you expecting? React doesn’t know the first thing about how you intend to use your custom component.
Components that want to expose their DOM nodes have to opt in to it like this:
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
Naturally, the component can pass the received ref to any internal component it wants.
You’ve probably seen this before it you aren’t a complete newbie. In component libraries, it is a common pattern for low-level components like buttons, inputs, and so on, to forward their refs to their DOM nodes.
131. Can you use ‘forwardRef’ to forward multiple DOM node refs?
This isn’t explained in the docs, but I wondered about it and found a legit guide on StackOverflow.
It involves the “useImperativeHandle” hook, which I’m about to explain.
132. ‘useImperativeHandle’ lets you control which functionality you expose with ‘forwardRef’.
That’s right, “useImperativeHandle” is meant to be used together with “forwardRef” when you want to do some custom hacky shit.
Or you can use it just to limit what the parent component can do with the exposed DOM node, like so:
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose focus and nothing else
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
It’s an uncommon use case, so don’t worry too much about this hook.
133. Avoid changing DOM nodes managed by React.
If you stick to non-destructive actions like focusing and scrolling, you shouldn’t encounter any problems.
However, if you try to modify the DOM manually, you can risk conflicting with the changes React is making, which might result in a full crash.
Still, the docs acknowledge that sometimes this can’t be avoided, in which case you are recommended to put your hacker hat on before proceeding, for good luck.
Section 9: Effects
134. Effects are the second-to-last concept introduced in the docs.
When React Hooks were first released on February 2019, a lot of attention was put on the useEffect hook. That made sense at the time because useEffect was the point of contact between the new hooks paradigm and the familiar concept of the “component lifecycle.”
However, the overreliance on useEffect and the lack of best practices at the time led to an avalanche of problems. In short, every single React dev is scarred for life from reading unmaintainable piles of Effects. We will never heal from our pain.
By putting Effects at the end of the docs, the React team aims to fix things and subdue the excessive use and glorification of Effects.
135. There’s a difference between “Effect” and “side effect.”
Before diving deep into what an Effect is, let’s stop for a second and pay attention to the wording.
In the new docs, the thing that you add to a component with the useEffect hook is called an “Effect” (always capitalized).
By capitalizing “Effect”, it’s easy to distinguish it from the generic concept of “side effect”.
This is in contrast to the old docs, in which the thing that you add to a component with the useEffect hook was called “side effect” or just “effect.”
The old docs might have given the impression that side effects are only found on the useEffect hook. But that’s not true at all. Most side effects in React should be on event handlers instead.
136. Effects are side effects that are caused by rendering, not by user actions.
As a refresher, there are two kinds of code in React components:
- Rendering code (which is pure, except for the one edge-case at 99), and
- side effects (which actually do something).
Most of the time, as we just said, you should use event handlers when you want side effects.
But sometimes there’s no suitable user event. Enter Effects.
Effects let you specify side effects that are caused by the rendering of a component itself, rather than by a particular event caused by a user action.
137. Don’t rush to add Effects to your components.
The truth is that many React apps can get away with little Effects, provided they follow the best practices recommended in the new docs.
But why is it good to avoid Effects? Simply put, they are too powerful, and they often produce code that’s hard to understand.
In a sense, Effects are the fulfilment of the covenant between the React framework and the developer: We get the full power of JavaScript to influence React’s workings, yet we must exercise restraint to avoid being devoured by the Leviathan of complexity.
Think of Effects as a last resort “escape hatch.”
138. Effects are used to synchronize a component with an external system.
As we said before, you are free to use an Effect any time you need a side effect that’s caused by the rendering of a component. Go ahead, I won’t stop you.
But, according to the new docs, there’s only one reason why you would need a side effect that’s caused by the rendering of a component.
And that is: When you need to keep your component synchronized with an external system.
Here, external system means any piece of code that’s not controlled by React, such as:
- DOM APIs. Like timers managed with
setInterval()
, or event subscriptions usingwindow.addEventListener()
. - Third-party libraries. Like an animation library with an API like
animation.start()
andanimation.reset()
. - A network. Using the
fetch()
API to perform a network request.
Note: The fact that your component interacts with an external system is not enough reason to use an Effect. If the interaction with the external system can be handled by refs and event handlers instead, please do so.
139. Effects run after rendering.
Effects run at the end of the rendering process, after the browser paints the screen update. In other words, useEffect “delays” a piece of code from running until that render is reflected on the screen.
The React team, in all its wisdom, thinks that’s a good time to synchronize the React components with some external system. And it makes sense: React’s entire business is to make pixels dance on the screen, so any external system should back off until React is done.
But if your Effect is doing something visual and it causes some visual flickering, React also offers the useLayoutEffect hook, which blocks the browser from painting until your Effect has run.
140. How to write an Effect.
It’s easy. Just import and declare your effect:
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Code here will run after *every* render
});
return <div />;
}
When you are synchronizing a component with an external system, you could use a prop to define the sync behavior.
Let’s look at an example of that. Here’s a VideoPlayer component that synchronizes with the browser media API based on the “isPlaying” prop:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
Here’s what happens when VideoPlayer renders (either the first time or if it re-renders):
- First, React will update the screen.
- Then, React will run your Effect, which will call
play()
orpause()
depending on the value ofisPlaying
prop.
You can use a similar approach to wrap any non-React code (like jQuery plugins) into declarative React components.
141. Effects run after *every* render, unless you declare dependencies.
By default, Effects run after every render. Often, this is not what you want:
- Sometimes it’s slow. Synchronizing with an external system is not always instant, so you might want to skip doing it unless it’s necessary.
- Sometimes it’s wrong. For example, you don’t want to trigger a component fade-in animation on every render, but only when the component first appears.
In the example VideoPlayer above, the effect will run on every render, and that’s fine. But if you want to change that, add an array of dependencies as the second argument.
You should start by adding an empty array:
useEffect(() => {
// ...
}, []);
Then you’ll see that the linter is screaming at you about missing dependencies. But don’t panic! Just follow the instructions until your Effect looks like this:
useEffect(() => {
if (isPlaying) { // It's used here...
// ...
} else {
// ...
}
}, [isPlaying]); // ...so it must be declared here!
That’s it, you’re done! Specifying [isPlaying]
as the dependency array tells React that it should skip re-running your Effect if isPlaying
is the same as it was during the previous render.
The dependency array can contain multiple dependencies. React will only skip re-running the Effect if all of the dependencies you specify have exactly the same values as they had during the previous render. React compares the dependency values using the Object.is
comparison.
142. In React, dependencies choose you.
Notice that you can’t “choose” your dependencies.
As a React developer, your task is to write the Effect code. Your only choice regarding dependencies is:
- If you want your Effect to run at every render, don’t add an array of dependencies.
- If you want your Effect to not run at every render, add an array of dependencies and let React fill it.
That’s right, the array of dependencies must match what React expects. You should have no say on the matter. This behavior helps catch many bugs in your code.
Careful readers might ask: How does React decide what should be in the array of dependencies?
Technically, the array of dependencies is the list of all reactive values referenced inside the Effect. We’ll look at what “reactive value” means later.
Readers might also ask: Shouldn’t I be able to change the dependencies array to define precisely when an Effect runs?
Fear not. Soon, we’ll see a lot of techniques to tightly control when an Effect runs without messing with the dependencies array, including the new and almighty useEffectEvent hook!
143. The different behaviours with and without the dependency array.
As a summary, let’s look at how the behaviour changes with and without the dependency array:
useEffect(() => {
// This runs after every render
});
useEffect(() => {
// This runs only on mount (when the component appears)
}, []);
useEffect(() => {
// This runs on mount *and also* if either a or b have changed since the last render
}, [a, b]);
144. Refs and set functions are omitted from the dependency array.
In the example VideoPlayer above, you might have noticed that the Effect uses “ref”, but React doesn’t want to include “ref” as a dependency.
This is because the “ref” object has a stable identity. It never changes, so it will never by itself cause the Effect to re-run.
The set functions returned by useState also have stable identity, so they will be omitted too.
Note that this behavior depends on what the linter can see, as the docs exquisitely explain:
Omitting always-stable dependencies only works when the linter can “see” that the object is stable. For example, if
ref
was passed from a parent component, you would have to specify it in the dependency array. However, this is good because you can’t know whether the parent component always passes the same ref, or passes one of several refs conditionally. So your Effect would depend on which ref is passed.
145. Add a cleanup if needed.
Sometimes you need to specify how to stop, undo, or clean up the synchronization with the external system. For example, “connect” needs “disconnect”, “subscribe” needs “unsubscribe”, and “fetch” needs either “cancel” or “ignore”.
To do that, return a cleanup function from your Effect:
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
React will call an Effect’s cleanup function each time before the Effect runs again, and one final time when the component unmounts.
Some Effects don’t need a cleanup function. More often they do — but if not, React will behave as if you returned an empty cleanup function that doesn’t do anything.
146. Effects fire twice in development.
At the time of the First World War, Switzerland had a tiny standing army, but a populace of skilled riflemen. In 1912, Kaiser Wilhelm II asked what a militia of a quarter million Swiss would do if he invaded with an army of a half million Germans. Their response? “Shoot twice and go home.”
React’s Strict Mode is just as bold. Firing twice might be annoying, but it will help you fix bugs that you might have never conceived, making you effectively go home sooner.
Let’s look at one example: By remounting your component, React verifies that navigating away and back doesn’t break your code. Users are known to navigate back and forth for no reason at all, so this trick can save you countless hours!
If you’re dealing with a bug caused by Effects firing twice, the right question isn’t “how to run an Effect just once”, but “how to fix my Effect so that it works after remounting”.
Usually, the answer is to implement a cleanup function. The cleanup function should stop or undo whatever the Effect was doing.
The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).
147. How to cleanup an Effect that animates?
If your Effect animates something in, the cleanup function should reset the animation to the initial values:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
}, []);
In development, opacity will be set to 1
, then to 0
, and then to 1
again. This should have the same user-visible behavior as setting it to 1
directly, which is what would happen in production.
147. How to cleanup an Effect that fetches data?
If your Effect fetches something, the cleanup function should either abort the fetch or ignore its result:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
The docs explicitly say that an ignore flag is better than an AbortController, because the AbortController can be unreliable if there are asynchronous steps chained after the fetch.
147. Unless you’re building a library, don’t fetch data from Effects.
Ideally, the last example isn’t something you shouldn’t worry about because you shouldn’t write your own logic to fetch data on component mount. Just like routing, it’s not trivial to do well.
Why not? The docs offer plenty of reasons:
- Effects don’t run on the server, so it’s hard to make effect-based data fetching play along with server-side rendering.
- It’s easy to end up with “network waterfalls,” and it’s hard to support fetching data in parallel.
- It’s hard to do preloading or caching.
- There’s quite a bit of boilerplate code involved to avoid race conditions.
Instead, the docs suggest two alternatives:
- If you use a meta-framework (like Next.js, Gatsby, or Remix), use its built-in data fetching mechanism.
- Otherwise, consider using a client-side cache. Popular open source solutions include TanStack Query, useSWR, and React Router 6.4+.
148. Buying a product is not an Effect.
Sometimes, even if you write a cleanup function, there’s no way to prevent user-visible consequences of running the Effect twice:
useEffect(() => {
// Wrong: This Effect fires twice in development, exposing a problem in the code.
fetch('/api/buy', { method: 'POST' });
}, []);
You wouldn’t want to buy the product twice. However, the behavior of “firing twice in development” also exposes a good reason why you shouldn’t put this logic in an Effect. What if the user goes to another page and then presses Back? Your Effect would also run again.
Buying is not caused by rendering; it’s caused by a specific interaction. So you should delete the Effect and make the request from the “buy” button’s event handler.
Note that POST request are fine in Effects if they are not related to user events, but rather to the rendering of a component (for example a POST request to track analytics).
149. Each render has its own Effects.
You can think of useEffect as “attaching” a piece of behavior to the render output.
In React, “rendering” is like taking a snapshot of the UI in time. A component’s props, state, local variables, event handlers, and Effects are all calculated and attached to the render output, thanks to the magic of closures.
The new docs offer two deep dives into how this works for Effects.
150. You don’t need an Effect to cache expensive calculations.
If you have an expensive calculation on render, but you don’t want to recalculate it if some unrelated state variable changes, you can cache (or “memoize”) it by wrapping it in a useMemo hook.
If you define its dependencies, it will only run when they change.
Remember, whatever you put inside useMemo should be pure.
And as always, don’t optimize prematurely. The new docs have a nice guide to figure out is a calculation is expensive. As a rule of thumb, if some calculation takes 1 millisecond or more, it might benefit from useMemo.
151. Avoid chains of Effects.
Sometimes you might feel tempted to chain Effects that each adjust a piece of state based on other state, and trigger each other.
That’s not only inefficient, but it’s a clear signs that your codebase is possessed by Satan. Simply put, as your code evolves, you will run into cases where the “chain” you wrote doesn’t fit the new requirements, and you will resort to ritualistic poultry sacrifices in order to refactor that shit. Lord have mercy!
As a rule of thumb: Before creating a chain of Effects, try to calculate what you can during rendering, and adjust the state as much as possible in event handler.
But sometimes, the chain is unavoidable: For example, imagine a form with multiple dropdowns where the options of the next dropdown depend on the selected value of the previous dropdown, and you have to fetch the new values from the network. Then, a chain of Effects fetching data is appropriate because you are synchronizing with an external system.
Yes, even in this day and age, a form that’s anything but trivial can easily cook up a succulent Spaghetti Bolognese that would make your Italian grandmother proud.
152. Be careful when initializing your application with Effects.
Some logic should only run once when the app loads.
You might place it in an Effect in the top-level component. But this is no bueno! You’ll quickly discover that it runs twice in development.
There are two possible solutions. You are free to pick whichever works best for your application. Either this:
let didInit = false;
function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// Only runs once per app load
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}
Or this:
if (typeof window !== 'undefined') { // Check if we're running in the browser.
// Only runs once per app load
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
Check the docs for tradeoffs.
153. If your external system is a “store”, consider useSyncExternalStore.
If you are using an Effect to synchronize your component to something that could be described as an “external store” that you “subscribe into” — say something like Redux, or even some built-in browser API — then you could use useSyncExternalStore.
Now, I’m gonna be honest here. It’s not very clear to me why the new docs are pushing so much for this bizarre new hook. It’s not clear to me why it’s better than useEffect. Also, I had the impression that useSyncExternalStore was for library authors.
If I were you, I would ignore it for now. But keep your eyes open, this hook might become more useful in the future. Only time will tell.
154. An Effect’s lifecycle is different from a component’s lifecycle.
Every React component goes through the same lifecycle:
- A component mounts when it’s added to the screen.
- A component updates when it receives new props or state. This usually happens in response to an interaction.
- A component unmounts when it’s removed from the screen.
That’s a good way to think about components, but not about Effects.
If you are not a complete monster that ignores the dependency array rules, you would agree that an Effect describes how to synchronize an external system to a component’s rendering.
With that in mind, we can say that to write an Effect, all you need to do is to describe how to start synchronization and how to stop it. If you do it well, your Effect will be resilient to being started and stopped as many times as it’s needed.
Now you’re thinking with Effects!
155. Each Effect represents a separate synchronization process.
Resist the temptation of adding unrelated logic to your Effect only because this logic needs to run at the same time as an Effect you already wrote.
It’s more maintainable to have two Effects, if they represent separate processes.
156. Reactive values: Another way to look at dependencies.
Dear reader, thank you for making it this far in the article. I’m proud of you. Don’t worry, this is the last time we’ll redefine how Effects work, we are almost done.
So, what are reactive values?
Props, state, and all variables declared inside the component are reactive because they’re calculated during rendering and participate in the React data flow.
Any reactive value can change on a re-render, so reactive values used by an Effect must be included in dependencies array.
Effects can “react” to any values from the component body. (Title drop!)
Effects are also reactive blocks of code. They re-synchronize when the values you read inside of them change.
So, if your Effect uses a value that’s defined outside of the component, the linter won’t ask you to add it as a dependency.
157. Mutable values (including global variables) can’t be dependencies.
A mutable value like location.pathname
can’t be a dependency. It’s mutable, so it can change at any time completely outside of the React rendering data flow. This also breaks the rules of React because reading mutable data during rendering (which is when you calculate the dependencies) breaks purity of rendering.
For the same reason, a mutable value like ref.current
or things you read from it also can’t be a dependency.
158. What to do when you don’t want to re-synchronize?
Ok, here’s the big question. Let’s say you have some dependencies in your array but you don’t want them to trigger a synchronization. As a developer, you don’t have much control of the dependencies, so what can you do?
You could “prove” to the linter that these values aren’t reactive values. For example, by moving them outside of the component, or by moving them inside the Effect.
You could also avoid relying on objects and functions as dependencies. If you create objects and functions during rendering and then read them from an Effect, they will be different on every render.
159. Don’t ignore the linter.
Sometimes people take extreme measures to avoid re-synchronizing. If you have an existing codebase, you might have some Effects that suppress the linter like this:
useEffect(() => {
// ...
// eslint-disable-next-line react-hooks/exhaustive-dependencies
}, []);
There’s no need for that. The linter is your friend. If the linter suggests a dependency, but adding it causes a loop or some other issue, it doesn’t mean the linter should be ignored. It means you need to change the code inside (or outside) the Effect so that that value isn’t reactive and doesn’t need to be a dependency.
160. React’s ultimate weapon: useEffectEvent
Let’s finish our tour of Effects with a bang!
For eons, React codebases were corrupted with //estlint-disable-next-line
s as a way to deal with Effect. We, the React devs, have been waiting patiently for useEffectEvent
since February 2019!
I cannot overstate my joy. I’ll let the code speak for itself:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // All dependencies declared
// ...
}
Here, onVisit is an Effect Event. The code inside it isn’t reactive. This is why you can use numberOfItems (or any other reactive value!) without worrying that it will cause the surrounding code to re-execute on changes!
Mind you, this feature is still experimental. The React team has hinted that it will be released by the time the new React docs are out of beta, but that has not come to happen. Also, the upcoming React 19 won’t have it either.
It’s a sad state of affairs, but we do hope this gets released soon. Meanwhile, this workaround seems to be the best approach right now.
Closing thoughts
Folks, let me tell you something, when I was first starting off my programming career, I would’ve never thought that the React docs would be this good.
They were always good, but they were never this good.
I remember when the only way to learn about React was to go to those fancy conferences and mingle with the cool hackers. You could count yourself lucky if they didn’t hack your Palm Pilot in retaliation.
So I encourage you, young and old, to take some time off your day and read the new React docs.
And maybe, just maybe, you’ll find that computer programming is the activity that will bring peace and prosperity to our world.
But most importantly, you’ll have fun. And that’s what it’s all about. May the force be with you.
If you’re up for a challenge, go ahead and check the last topic from the docs, “Custom Hooks.”
Thanks for reading! If you enjoy humorous tech stories like these and want to support me to keep writing forever, consider signing up to become a Medium member. It’s $5 a month, giving you unlimited access to stories on Medium. If you sign up using my link, I’ll earn a small commission. You can also follow me on Medium and Twitter.