Watch "Use the key prop when Rendering a List with React" on egghead.io (part of The Beginner's Guide to ReactJS).
Play around with this form:
Specifically, try changing the subject, then switch the topic and notice that the value in the input field doesn't change to a more sensible subject. Even if you type something like "My company needs training" and then changing the topic from "Training" to "Question" it would make more sense to have it reset the subject to a better default.
Now try this one:
That's working as expected now. Here's the implementation, and I'll highlight the difference:
const defaultValuesByTopic = {
training: 'I would like some training',
consulting: 'I have consulting needs',
question: 'I have some questions',
}
function Contact() {
const [topic, setTopic] = React.useState('training')
return (
<form>
<label htmlFor="topic">Topic</label>
<select
id="topic"
value={topic}
onChange={(e) => setTopic(e.target.value)}
>
<option value="training">Training</option>
<option value="consulting">Consulting</option>
<option value="question">Question</option>
</select>
<label htmlFor="subject">Email Subject</label>
<input
id="subject"
key={topic}
defaultValue={defaultValuesByTopic[topic]}
/>
<label htmlFor="body">Email body</label>
<textarea id="body" />
</form>
)
}
The only difference between these implementations is that the working one has a
key
prop and the other does not.
I want to share a little trick with you, not because I use this a lot (though
this is exactly what I do on my contact page), but because
understanding this principle will help you understand React a bit better. It has
to do with React component "instances" and how React treats the key
prop.
What I'm about to show you has a lot to do with element/component instances and
applies just as much to <input />
s like above as it does to the components you
write and render. It may be a bit easier to understand with component state, so
that's the angle we're going to approach this from.
Imagine you've got a React component that manages internal state. That state is attached to the component instance. This is why you can render that component twice on the page and they will operate completely independently. For our demonstration, let's use something really simple:
function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount((c) => c + 1)
return <button onClick={increment}>{count}</button>
}
We could render this many times on the page and each would be completely independent. React will store the state with each individual instance. When one component is removed from the page, it won't affect others. If you render a new one, it doesn't affect existing components.
You may know that React's key
prop is something you need to put on elements
when you map over an array (otherwise React will get mad at you).
Side note: If you'd like to know why this is necessary and what can happen if
you ignore it or simply put the index
as the key, watch "Use the key prop
when Rendering a List with
React"
React's key
prop gives you the ability to control component instances. Each
time React renders your components, it's calling your functions to retrieve the
new React elements that it uses to update the DOM. If you return the same
element types, it keeps those components/DOM nodes around, even if all the props
changed.
For more on this, read One simple trick to optimize React re-renders
That asterisk on the word "all" above is what I want to talk about here. The
exception to this is the key
prop. This allows you to return the exact same
element type, but force React to unmount the previous instance, and mount a new
one. This means that all state that had existed in the component at the time is
completely removed and the component is "reinitialized" for all intents and
purposes. For components, this means that React will run cleanup on effects (or
componentWillUnmount
), then it will run state initializers (or the
constructor
) and effect callbacks (or componentDidMount
).
NOTE: effect cleanup actually happens after the new component has been mounted, but before the next effect callback is run.
Here's a simple example of this working in a counter:
function Counter() {
console.log('Counter called')
const [count, setCount] = React.useState(() => {
console.log('Counter useState initializer')
return 0
})
const increment = () => setCount((c) => c + 1)
React.useEffect(() => {
console.log('Counter useEffect callback')
return () => {
console.log('Counter useEffect cleanup')
}
}, [])
console.log('Counter returning react elements')
return <button onClick={increment}>{count}</button>
}
function CounterParent() {
// using useReducer this way basically ensures that any time you call
// setCounterKey, the `counterKey` is set to a new value which will
// make the `key` different resulting in React unmounting the previous
// component and mounting a new one.
const [counterKey, setCounterKey] = React.useReducer((c) => c + 1, 0)
return (
<div>
<button onClick={setCounterKey}>reset</button>
<Counter key={counterKey} />
</div>
)
}
And here's that rendered out:
Here's an annotated example of what would be logged if I click the counter button, then click reset:
// getting mounted
Counter called
Counter useState initializer
Counter returning react elements
// now it's mounted
Counter useEffect callback
// click the counter button
Counter called
Counter returning react elements
// notice the initializer and effect callback are not called this time
// click the reset button in the parent
// these next logs are happening for our new instance
Counter called
Counter useState initializer
Counter returning react elements
// cleanup old instance
Counter useEffect cleanup
// new instance is now mounted
Counter useEffect callback
Conclusion
Again, this happens just as much for the state of native form elements (for
things like value
and even focus). The key
prop isn't just for getting rid
of that annoying React console error when you try to render an array of elements
(all "annoying" errors from React are awesome and help you avoid bugs, so please
do not ignore them). The key
prop can also be a useful mechanism for
controlling React component and element instances.
I hope that was interesting/enlightening. If you want to play around with any of this code, I have a codesandbox for it here. Have fun!