A couple years ago, I was teaching React at a local coding bootcamp, and I noticed that there were a handful of things that kept catching students off guard. People kept falling into the same pits!
In this tutorial, we're going to explore 9 of the most common gotchas. You'll learn how to steer around them, and hopefully avoid a lot of frustration.
In order to keep this blog post light and breezy, we won't dig too much into the reasons behind these gotchas. This is more of a quick reference.
Link to this headingEvaluating with zero
Alright, let's start with one of the most pervasive gotchas. I've seen this one “in the wild” on a handful of production apps!
Take a look at the following setup:
Our goal is to conditionally show a shopping list. If we have at least 1 item in the array, we should render a
ShoppingList element. Otherwise, we shouldn't render anything.
And yet, we wind up with a random
0 in the UI!
This happens because
items.length evaluates to
&& operator short-circuits, and the entire expression resolves to
It's effectively as if we had done this:
Unlike other falsy values (
false, etc), the number 0 is a valid value in JSX. After all, there are plenty of scenarios in which we really do want to print the number
How to fix it: Our expression should use a “pure” boolean value (true/false):
items.length > 0 will always evaluate to either
false, and so we'll never have any issues.
Alternatively, we can use a ternary expression:
Both options are perfectly valid, and it comes down to personal taste.
Link to this headingMutating state
Let's keep working with our shopping list example. Suppose we have the ability to add new items:
handleAddItem function is called whenever the user submits a new item. Unfortunately, it doesn't work! When we enter an item and submit the form, that item is not added to the shopping list.
Here's the problem: we're violating maybe the most sacred rule in React. We're mutating state.
Specifically, the problem is this line:
React relies on an state variable's identity to tell when the state has changed. When we push an item into an array, we aren't changing that array's identity, and so React can't tell that the value has changed.
How to fix it: We need to create a brand new array. Here's how I'd do it:
Instead of modifying an existing array, I'm creating a new one from scratch. It includes all of the same items (courtesy of the
... spread syntax), as well as the newly-entered item.
The distinction here is between editing an existing item, versus creating a new one. When we pass a value to a state-setter function like
setCount, it needs to be a new entity.
The same thing is true for objects:
... syntax is a way to copy/paste all of the stuff from an array/object into a brand new entity. This ensures that everything works properly.
Link to this headingNot generating keys
Here's a warning you've likely seen before:
The most common way for this to happen is when mapping over data. Here's an example of this violation:
Whenever we render an array of elements, we need to provide a bit of extra context to React, so that it can identify each item. Critically, this needs to be a unique identifier.
Many online resources will suggest using the array index to solve this problem:
I don't think this is good advice. This approach will work sometimes, but it can cause some pretty big problems in other circumstances.
As you gain a deeper understanding of how React works, you'll be able to tell whether it's fine or not on a case-by-case basis, but honestly, I think it's easier to solve the problem in a way which is always safe. That way, you never have to worry about it!
Here's the plan: Whenever a new item is added to the list, we'll generate a unique ID for it:
crypto.randomUUID is a method built into the browser (it's not a third-party package). It's available in all major browsers. It has nothing to do with cryptocurrencies.
This method generates a unique string, like
By dynamically generating an ID whenever the user submits the form, we're guaranteeing that each item in the shopping list has a unique ID.
Here's how we'd apply it as the key:
Importantly, we want to generate the ID when the state is updated. We don't want to do this:
Generating it in the JSX like this will cause the key to change on every render. Whenever the key changes, React will destroy and re-create these elements, which can have a big negative impact on performance.
This pattern — generating the key when the data is first created — can be applied to a wide range of situations. For example, here's how I'd create unique IDs when fetching data from a server:
Link to this headingMissing whitespace
Here's a dastardly gotcha I see all the time on the web.
Notice that the two sentences are all smushed together:
How to fix it: we need to add an explicit space character between the text and the anchor tag:
One little pro-tip: if you use Prettier, it'll add these space characters for you automatically! Just be sure to let it do the formatting (don't pre-emptively split things onto multiple lines).
Link to this headingAccessing state after changing it
This one catches everyone off-guard at some point or other. When I taught at a local coding bootcamp, I lost track of the number of times people came to me with this issue.
Here's a minimal counter application: clicking on the button increments the count. See if you can spot the problem:
After incrementing the
count state variable, we're logging the value to the console. Curiously, it's logging the wrong value:
Here's the problem: state-setter function in React like
setCount are asynchronous.
This is the problematic code:
It's easy to mistakenly believe that
setCount functions like assignment, as though it was equivalent to doing this:
This isn't how React is built though. When we call
setCount, we aren't re-assigning a variable. We're scheduling an update.
It can take a while for us to fully wrap our heads around this idea, but here's something that might help it click: we can't reassign the
count variable, because it's a constant!
So how do we fix this? Fortunately, we already know what this value should be. We need to capture it in a variable, so that we have access to it:
I like using the “next” prefix whenever I do stuff like this (
nextEmail, etc). It makes it clearer to me that we're not updating the current value, we're scheduling the next value.
Link to this headingReturning multiple elements
Sometimes, a component needs to return multiple top-level elements.
We want our
LabeledInput component to return two elements: a
<label> and an
<input>. Frustratingly, we're getting an error:
How do we fix it? For a long time, the standard practice was to wrap both elements in a wrapper tag, like a
By grouping our
<input> in a
<div>, we're only returning a single top-level element!
Here's what it looks like in plain JS:
With this new approach, we're returning a single element, and that element contains two children elements. Problem solved!
We can make this solution even better using fragments:
React.Fragment is a React component that exists purely to solve this problem. It allows us to bundle up multiple top-level elements without affecting the DOM. This is great: it means we aren't polluting our markup with an unnecessary
It also has a convenient shorthand. We can write fragments like this:
I like the symbolism here: the React team chose to use an empty HTML tag,
<>, as a way of showing that fragments don't produce any real markup.
Link to this headingFlipping from uncontrolled to controlled
Let's look at a typical form, binding an input to a piece of React state:
If you start typing in this input, you'll notice a console warning:
Here's how to fix it: We need to initialize our
When we set the
value attribute, we tell React that we want this to be a controlled input. That only works when we pass it a defined value, though! By initializing
value is never being set to
Link to this headingMissing style brackets
JSX is made to look and feel quite a lot like HTML, but there are some surprising differences between the two that tend to catch people offguard.
Most of the differences are well-documented, and the console warnings tend to be very descriptive and helpful. If you accidentally use
class instead of
className, for example, React will tell you exactly what the problem is.
But there's one subtle difference that tends to trip people up: the
style is written as a string:
In JSX, however, we need to specify it as an object, with camelCased property names.
Below, I've tried to do exactly this, but I wind up with an error. Can you spot the mistake?
The problem is that I need to use double squigglies, like this:
To understand why this is necessary, we need to dig into this syntax a bit.
In JSX, we use squiggly brackets to create an expression slot. We can put any valid JS expression in this slot. For example:
Whatever we put between the
className will either be
'btn primary' or
I think it's clearer if we pull the object out into a variable:
The outer set of squigglies creates an “expression slot” in the JSX. The inner set creates a JS object that holds our styles.
Link to this headingAsync effect function
Let's suppose we have a function which fetches some user data from our API on mount. We'll use the
useEffect hook, and we want to use the
Here's my first shot at it:
Unfortunately, we get an error:
Alright, that's no problem. Let's update the effect callback to be an async function, by prefixing it with the
Unfortunately, this doesn't work either; we get a cryptic error message:
Here's the fix: We need to create a separate async function within our effect:
To understand why this workaround is necessary, it's worth considering what the
async keyword actually does.
For example, what would you guess this function returns?
At first glance, it seems obvious: it returns the string
"Hello world!"! But actually, this function returns a promise. That promise resolves to the string
This is a problem, because the
useEffect hook isn't expecting us to return a promise! It expects us to return either nothing (like we are above), or a cleanup function.
Cleanup functions are well beyond the scope of this tutorial, but they're incredibly important. Most of our effects will have some sort of teardown logic, and we need to provide it to React ASAP, so that React can invoke it when the dependencies change, or the component unmounts.
With our "separate async function" strategy, we're still able to return a cleanup function right away:
You can name this function whatever you like, but I like the generic name
runEffect. It makes clear that it holds the primary effect logic.
Link to this headingDeveloping an intuition
At first glance, a lot of the fixes we've seen in this tutorial seem pretty arbitrary. Why, exactly, do we need to provide a unique key? How come we can't access state after changing it? And why on earth is
useEffect so dang finicky?!
React has always been pretty tricky to become truly comfortable with, and it's especially true nowadays with hooks. It takes a while for everything to click.
I started using React back in 2015, and I remember thinking: “This is friggin’ cool, but I have no idea how this works.” 😅
Since then, I've been building my mental model of React one puzzle piece at a time. I've had a series of epiphanies, and each time, my mental model has become more sturdy, more robust. I began to understand why React works the way it does.
I found I didn't have to keep memorizing arbitrary rules; instead, I could rely on my intuition. It's hard to overstate how much more fun React became for me!
For the past year, I've been developing an interactive self-paced online course called The Joy of React. It's a beginner-friendly course with one goal: to help you build your intuition of how React works, so that you can use it to build rich, dynamic web applications.
My courses aren't like other courses; you won't sit and watch me code for hours and hours. The Joy of React mixes lots of different media types: there are videos, sure, but there are also interactive articles, challenging exercises, real-world-inspired projects, and even a mini-game or two.
The Joy of React will be released in a few months. You can learn much more about it, and sign up for updates, on the course homepage:
March 15th, 2023