How React Uses Closures to Avoid Bugs
The sneaky, surreptitious bug that React saved us from by using closures.
Here’s a simple React component:
type User = { firstName: string; lastName: string }function UserCard(props: { user: User }) { return ( <div className="user-card"> <h2> {props.user.firstName} {props.user.lastName} </h2> </div> )}
Now, let’s say the boss asks you to please lowercase the user’s name. There are a few ways we could go about doing this, but it begs the question, could we just modify the props?
type User = { firstName: string; lastName: string }function UserCard(props: { user: User }) { props.user.firstName = props.user.firstName.toLowerCase() props.user.lastName = props.user.lastName.toLowerCase() return ( <div className="user-card"> <h2> {props.user.firstName} {props.user.lastName} </h2> </div> )}
Technically, yes, this will correctly lowercase the user’s first and last name, however, we need to talk about encapsulation.
Encapsulation is a very important concept in programming. It is the idea that separate components are isolated from one another. In React, this allows the components to be reusable. The parent component doesn't need to understand the implementation details of the component it renders because of this isolation. And the component itself doesn't need to know about the parent component.
However, if the props passed down to the parent component are modified, this breaks that encapsulation and can present surprises. For example:
type User = { firstName: string; lastName: string }function UserCard(props: { user: User }) { props.user.firstName = props.user.firstName.toLowerCase() props.user.lastName = props.user.lastName.toLowerCase() return ( <div className="user-card"> <h2> {props.user.firstName} {props.user.lastName} </h2> </div> )}function App() { const user = { firstName: 'Bob', lastName: 'Jones' } return ( <div> <UserCard user={user} /> <button onClick={() => alert(`Hello, ${user.firstName}!`)}>Greet</button> </div> )}
When you click the button, you’ll get Hello, bob
. This is surprising behavior if you don’t know what the UserCard
component is doing.
Now, we could avoid this if we do things a little differently here. How about instead of passing the user object, we pass the firstName
and lastName
properties directly? We could do <UserCard firstName={user.firstName} lastName={user.lastName} />
or even just {...user}
which is effectively the same thing.
function UserCard(props: { firstName: string; lastName: string }) { props.firstName = props.firstName.toLowerCase() props.lastName = props.lastName.toLowerCase() return ( <div className="user-card"> <h2> {props.firstName} {props.lastName} </h2> </div> )}function App() { const user = { firstName: 'Bob', lastName: 'Jones' } return ( <div> <UserCard {...user} /> <button onClick={() => alert(`Hello, ${user.firstName}!`)}>Greet</button> </div> )}
This will not suffer from the same issue, but we’ll get an error from React:
TypeError: Cannot assign to read only property 'firstName' of object '#<Object>'
This is because the props object React passes to your component has read only properties (using Object.defineProperties). This is React helping us avoid breaking encapsulation. By modifying props it reduces our confidence in the one-way data flow. It’s not a big issue with the way we’re doing things here, but what if we did something like this:
function UserCard(props: { firstName: string; lastName: string }) { return ( <div className="user-card"> <h2> {props.firstName} {props.lastName} </h2> <button onClick={() => { props.firstName = props.firstName.toLowerCase() props.lastName = props.lastName.toLowerCase() }} > lowercase them </button> </div> )}
What’s supposed to happen in the onClick
? Presumably we want the UI to show the names as lower cased, but that wouldn’t happen because in React mutation is not a mechanism for re-rendering. This is one of my favorite parts of React honestly, but I digress.
So, React isn’t allowing us to modify the props, and modifying properties of an object passed through props can lead to issues as well. So what can we do? Well, here’s something we could do:
function UserCard(props: { firstName: string; lastName: string }) { const lowerFirstName = props.firstName.toLowerCase() const lowerLastName = props.lastName.toLowerCase() return ( <div className="user-card"> <h2> {lowerFirstName} {lowerLastName} </h2> </div> )}
That works fine. Or you could also just inline it:
function UserCard(props: { firstName: string; lastName: string }) { return ( <div className="user-card"> <h2> {props.firstName.toLowerCase()} {props.lastName.toLowerCase()} </h2> </div> )}
Or, you could even do this:
function UserCard({ firstName, lastName,}: { firstName: string lastName: string}) { firstName = firstName.toLowerCase() lastName = lastName.toLowerCase() return ( <div className="user-card"> <h2> {firstName} {lastName} </h2> </div> )}
Now this last one might have you surprised a bit. We’re destructuring the props, but then we’re reassigning them. How is this different from modifying props? It’s different because we’re not actually changing the values on the props object, we’re simply reassigning the variables we made in the process of destructuring.
I find myself doing this occasionally if I don’t want to bother creating and naming a separate variable and I have no need of the original value. And because it’s a variable reassignment rather than an object mutation, it doesn’t suffer from the problem React is trying to protect us against.
So there you are. This is why React doesn’t allow you to mutate props and why you should try. In general, there are only a few situations where you would consider mutation in a React application (I’m looking at you useRef
👀). For the most part, rely on React’s single-way data flow and explicit state updates through setState
via useState
and dispatch
via useReducer
and you’ll find the data flow in your application is much simpler to follow.
Cheers!
Delivered straight to your inbox.
The sneaky, surreptitious bug that React saved us from by using closures.
How and why I import react using a namespace
Forms can get slow pretty fast. Let's explore how state colocation can keep our React forms fast.
Simplify and speed up your app development using React composition