The React Key Prop: How to reset component state, remove unnecessary effects, and write clearer code
If you've done any kind of frontend work, you've probably come across a form.
They're not difficult to implement in React. That said, there are edge cases that can lead to complex code if you're not careful.
One edge case involves using a single form for different parts of your app. For example, having a single form for a create and edit flow might make sense, but it introduces complexity. Trying to manage the state between these two forms can be tricky.
Let's look at an example to get a better idea.
In this case, we're working with a contact list application.
On the left side of the app is a list of all your contacts. At the bottom of that list is a button to add a new contact. When you click the button, a form comes up on the right side.
Pretty basic, but this form has another use.
It also works as an edit contact form. When you click the name of a contact, we'll show the same form but populate the form fields with existing data. The form is the same component. The difference is in how it's initialized.
The app might look something like this:
Looks simple, but what happens if you’re editing a contact and then click the Add New button?
Remember, both forms use the same component. When React re-renders the form, it'll see that an instance of the form already exists and try to reuse it.
React will try to re-render the form in place
Normally this is a good thing because it helps keep the render cycle fast.
In this case, not so much.
The problem is that we need to clear the form state when we switch from an edit to a create. A common approach is to use an effect to handle the reset logic. Anytime some identifier changes, we'll tell a useEffect
hook to reset the state.
Here's what that might look like:
function Form(props) {
const [firstName, setFirstName] = React.useState(props.firstName);
const [lastName, setLastName] = React.useState(props.lastName);
const [phone, setPhone] = React.useState(props.phone);
const [email, setEmail] = React.useState(props.email);
// When the userId changes we reset the state
React.useEffect(() => {
setFirstName(props.firstName);
setLastName(props.lastName);
setPhone(props.phone);
setEmail(props.email);
}, [props.userId]);
return <div>{/* form goes here */}</div>;
}
// Props is the user id and the existing form state
// user id is `_new` if we're adding a new contact
function Contacts(props) {
return <MyForm {...props} />;
}
While this works, it’s not ideal. We end up with an extra render pass which can lead to a flash of stale content in some cases. We also have to add extra code for the useEffect
.
There’s simpler way to address this problem.
We can use React’s key prop.
The key
prop is a special attribute that acts as a unique identifier for components.
With this identifier we can change React's default render behavior. Instead of reusing a component during a render cycle, React will build a new instance. If the key
changes, React will unmount the old instance and mount a new one.
Ok. So why is that useful?
It's useful because it lets us take advantage of events that happen when a component mounts. In this case, we get to redeclare state via the useState
hook. If React used the same component instance during the re-render phase then this wouldn't be possible.
In other words, you don't need an effect to reset state.
Here’s what that would look like:
function Form(props) {
const [firstName, setFirstName] = React.useState(props.firstName);
const [lastName, setLastName] = React.useState(props.lastName);
const [phone, setPhone] = React.useState(props.phone);
const [email, setEmail] = React.useState(props.email);
// NO EFFECT!
return <div>{/* form goes here */}</div>;
}
function Contacts(props) {
const { userId, ...formState } = props;
// When the `key` prop changes, React will unmount and
// remount the component instead of re-render in place.
return <MyForm key={userId} {...formState} />;
}
The relevant change is the use of a key
prop on the MyForm
component. Whenever the key changes, React will unmount the component and remount a new instance. This means that the useState
hooks are initialized again, so we don't need the effect.
Don’t Reset State. Reinitialize It.
The useEffect
hook is a powerful tool, but you don’t always need it.
If you need to reset the state of a component, try using a key
prop instead. Treat the component as a different instance and let React build it from scratch. Reinitialize the state, don’t reset it.
You’ll end up with less code that’s easier to reason about.