Collection of react pitfalls as described in react documentation
React components are regular JavaScript functions, but their names must start with a capital letter or they won’t work!
source: https://react.dev/learn/your-first-component#step-2-define-the-function
Without parentheses, any code on the lines after return will be ignored!
return (
<div>
<img src="https://i.imgur.com/MK3eW3As.jpg" alt="Katherine Johnson" />
</div>
);
source: https://react.dev/learn/your-first-component#step-3-add-markup
Components can render other components, but you must never nest their definitions:
export default function Gallery() {
// 🔴 Never define a component inside another component!
function Profile() {
// ...
}
// ...
}
The snippet above is very slow and causes bugs. Instead, define every component at the top level:
export default function Gallery() {
// ...
}
// ✅ Declare components at the top level
function Profile() {
// ...
}
When a child component needs some data from a parent, pass it by props instead of nesting definitions.
source: https://react.dev/learn/your-first-component#nesting-and-organizing-components
For historical reasons, aria-* and data-* attributes are written as in HTML with dashes.
source: https://react.dev/learn/writing-markup-with-jsx#3-camelcase-salls-most-of-the-things
Inline style
properties are written in camelCase. For example, HTML <ul style="background-color: black">
would be written as <ul style={{ backgroundColor: 'black' }}>
in your component.
Don’t miss the pair of {
and }
curlies inside of (
and )
when declaring props:
function Avatar({ person, size }) { // ...}
This syntax is called “destructuring” and is equivalent to reading properties from a function parameter:
function Avatar(props) {
let person = props.person;
let size = props.size;
// ...}
source: https://react.dev/learn/passing-props-to-a-component#step-2-read-props-inside-the-child-component
Don’t put numbers on the left side of &&
.
To test the condition, JavaScript converts the left side to a boolean automatically. However, if the left side is 0
, then the whole expression gets that value (0
), and React will happily render 0
rather than nothing.
For example, a common mistake is to write code like messageCount && <p>New messages</p>
. It’s easy to assume that it renders nothing when messageCount
is 0
, but it really renders the 0
itself!
To fix it, make the left side a boolean: messageCount > 0 && <p>New messages</p>
.
source: https://react.dev/learn/conditional-rendering#logical-and-operator-
Arrow functions implicitly return the expression right after =>
, so you didn’t need a return
statement:
const listItems = chemists.map(person =>
<li>...</li> // Implicit return!
);
However, you must write return
explicitly if your =>
is followed by a {
curly brace!
const listItems = chemists.map(person => { // Curly brace
return <li>...</li>;
});
Arrow functions containing => {
are said to have a “block body”. They let you write more than a single line of code, but you have to write a return
statement yourself. If you forget it, nothing gets returned!
source: https://react.dev/learn/rendering-lists#filtering-arrays-of-items
You might be tempted to use an item’s index in the array as its key. In fact, that’s what React will use if you don’t specify a key
at all. But the order in which you render items will change over time if an item is inserted, deleted, or if the array gets reordered. Index as a key often leads to subtle and confusing bugs.
Similarly, do not generate keys on the fly, e.g. with key={Math.random()}
. This will cause keys to never match up between renders, leading to all your components and DOM being recreated every time. Not only is this slow, but it will also lose any user input inside the list items. Instead, use a stable ID based on the data.
Note that your components won’t receive key
as a prop. It’s only used as a hint by React itself. If your component needs an ID, you have to pass it as a separate prop: <Profile key={id} userId={id} />
.
source: https://react.dev/learn/rendering-lists#why-does-react-need-keys
Functions passed to event handlers must be passed, not called. For example:
passing a function (correct) | calling a function (incorrect) |
---|---|
<button onClick={handleClick}> |
<button onClick={handleClick()}> |
The difference is subtle. In the first example, the handleClick
function is passed as an onClick
event handler. This tells React to remember it and only call your function when the user clicks the button.
In the second example, the ()
at the end of handleClick()
fires the function immediately during rendering, without any clicks. This is because JavaScript inside the JSX {
and }
executes right away.
When you write code inline, the same pitfall presents itself in a different way:
passing a function (correct) | calling a function (incorrect) |
---|---|
<button onClick={() => alert('...')}> |
<button onClick={alert('...')}> |
Passing inline code like this won’t fire on click—it fires every time the component renders:
// This alert fires when the component renders, not when clicked!
<button onClick={alert('You clicked me!')}>
If you want to define your event handler inline, wrap it in an anonymous function like so:
<button onClick={() => alert('You clicked me!')}>
Rather than executing the code inside with every render, this creates a function to be called later.
In both cases, what you want to pass is a function:
<button onClick={handleClick}>
passes thehandleClick
function.<button onClick={() => alert('...')}>
passes the() => alert('...')
function.
Read more about arrow functions.
source: https://react.dev/learn/responding-to-events#adding-event-handlers
All events propagate in React except onScroll
, which only works on the JSX tag you attach it to.
source: https://react.dev/learn/responding-to-events#event-propagation
Hooks—functions starting with use
—can only be called at the top level of your components or your own Hooks. You can’t call Hooks inside conditions, loops, or other nested functions. 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.
source: https://react.dev/learn/state-a-components-memory#meet-your-first-hook
Rendering must always be a pure calculation:
- Same inputs, same output. Given the same inputs, a component should always return the same JSX. (When someone orders a salad with tomatoes, they should not receive a salad with onions!)
- It minds its own business. It should not change any objects or variables that existed before rendering. (One order should not change anyone else’s order.)
Otherwise, you can encounter confusing bugs and unpredictable behavior as your codebase grows in complexity. When developing in “Strict Mode”, React calls each component’s function twice, which can help surface mistakes caused by impure functions.
Unfortunately, slice
and splice
are named similarly but are very different:
slice
lets you copy an array or a part of it.splice
mutates the array (to insert or delete items).
In React, you will be using slice
(no p
!) a lot more often because you don’t want to mutate objects or arrays in state. Updating Objects explains what mutation is and why it’s not recommended for state.
source: https://react.dev/learn/updating-arrays-in-state#updating-arrays-without-mutation
If your state variable is an object, remember that you can’t update only one field in it without explicitly copying the other fields. For example, you can’t do setPosition({ x: 100 })
in the above example because it would not have the y
property at all! Instead, if you wanted to set x
alone, you would either do setPosition({ ...position, x: 100 })
, or split them into two state variables and do setX(100)
.
source: https://react.dev/learn/choosing-the-state-structure#group-related-state
Remember that it’s the position in the UI tree—not in the JSX markup—that matters to React! This component has two return
clauses with different <Counter />
JSX tags inside and outside the if
:
import { useState } from 'react';
export default function App() {
const [isFancy, setIsFancy] = useState(false);
if (isFancy) {
return (
<div>
<Counter isFancy={true} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
return (
<div>
<Counter isFancy={false} />
<label>
<input
type="checkbox"
checked={isFancy}
onChange={e => {
setIsFancy(e.target.checked)
}}
/>
Use fancy styling
</label>
</div>
);
}
function Counter({ isFancy }) {
const [score, setScore] = useState(0);
const [hover, setHover] = useState(false);
let className = 'counter';
if (hover) {
className += ' hover';
}
if (isFancy) {
className += ' fancy';
}
return (
<div
className={className}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
>
<h1>{score}</h1>
<button onClick={() => setScore(score + 1)}>
Add one
</button>
</div>
);
}
You might expect the state to reset when you tick checkbox, but it doesn’t! This is because both of these <Counter />
tags are rendered at the same position. React doesn’t know where you place the conditions in your function. All it “sees” is the tree you return.
In both cases, the App
component returns a <div>
with <Counter />
as a first child. To React, these two counters have the same “address”: the first child of the first child of the root. This is how React matches them up between the previous and next renders, regardless of how you structure your logic.
This is why you should not nest component function definitions.
Here, the MyTextField
component function is defined inside MyComponent
:
import { useState } from 'react';
export default function MyComponent() {
const [counter, setCounter] = useState(0);
function MyTextField() {
const [text, setText] = useState('');
return (
<input
value={text}
onChange={e => setText(e.target.value)}
/>
);
}
return (
<>
<MyTextField />
<button onClick={() => {
setCounter(counter + 1)
}}>Clicked {counter} times</button>
</>
);
}
Every time you click the button, the input state disappears! This is because a different MyTextField
function is created for every render of MyComponent
. You’re rendering a different component in the same position, so React resets all state below. This leads to bugs and performance problems. To avoid this problem, always declare component functions at the top level, and don’t nest their definitions.
By default, Effects run after every render. This is why code like this will produce an infinite loop:
const [count, setCount] = useState(0);useEffect(() => { setCount(count + 1);});
Effects run as a result of rendering. Setting state triggers rendering. Setting state immediately in an Effect is like plugging a power outlet into itself. The Effect runs, it sets the state, which causes a re-render, which causes the Effect to run, it sets the state again, this causes another re-render, and so on.
Effects should usually synchronize your components with an external system. If there’s no external system and you only want to adjust some state based on other state, you might not need an Effect.
source: https://react.dev/learn/synchronizing-with-effects#step-1-declare-an-effect
The behaviors without the dependency array and with an empty []
dependency array are different:
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]);
source: https://react.dev/learn/synchronizing-with-effects#step-2-specify-the-effect-dependencies
The linter is your friend, but its powers are limited. The linter only knows when the dependencies are wrong. It doesn’t know the best way to solve each case. If the linter suggests a dependency, but adding it causes a loop, it doesn’t mean the linter should be ignored. 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.
If you have an existing codebase, you might have some Effects that suppress the linter like this:
useEffect(() => { // ... // 🔴 Avoid suppressing the linter like this: // eslint-ignore-next-line react-hooks/exhaustive-deps}, []);
On the next pages, you’ll learn how to fix this code without breaking the rules. It’s always worth fixing!
If you have an existing codebase, you might have some Effects that suppress the linter like this:
useEffect(() => { // ... // 🔴 Avoid suppressing the linter like this: // eslint-ignore-next-line react-hooks/exhaustive-deps}, []);
When dependencies don’t match the code, there is a very high risk of introducing bugs. By suppressing the linter, you “lie” to React about the values your Effect depends on.
Instead, use the techniques below.
source: https://react.dev/learn/removing-effect-dependencies#to-change-the-dependencies-change-the-code