Leveraging React Keys to handle animation triggers
If you've been working with React for a while, you will be very familiar with the missing "key" prop error which will always scream at you when you forget to the add the aforementioned key
prop to elements being rendered using an iterator like Array.map()
or similar. However did you know that keys can also be super useful for solving a super irritating problem — triggering CSS animations when props change.
React key prop superpowers
The key
prop is used by React to aid in determining what has changed when dynamic data is updated, sorted or filtered. Typically the data you're working with will or should include a unique identifier that is suitable to use for the key={item.uuid}
prop when rendering in this manner, however you can also use the key prop outside of lists to tell React when dynamic data to a component has changed. When a key is used in this way and the key
value changes, React will destroy the DOM nodes that are attached and recreate them. This provides us with a super useful API for forcing a render and resetting local state without worrying about keeping track in a local state variable in the child component itself.
Example
In the below example we have a Panel()
functional component which accepts 2 props; title and content. This component will update based on user interaction with a parent component, think of this as a landscape oriented accordian, where the left has a vertical list of subjects, and the panel displays the related content. You can find the CodeSandbox embedded below the example code, if you want to fiddle. We want this component to have a (ugly) bounce in animation applied to the title when the users clicks between the subjects.
// app.js
function Panel({ title, content }) {
return (
<div className={`panel`}>
<h2 className={`animate`}>{title}</h2>
<p>{content}</p>
</div>
);
}
export default function App() {
const [activePane, setActivePane] = useState(DATA[0].id);
const panelData = DATA.filter((item) => item.id === activePane)[0];
return (
<div className="App">
<ul>
{DATA.map(({ id, title }) => (
<li key={id}>
<button onClick={() => setActivePane(id)}>{title}</button>
</li>
))}
</ul>
{panelData && (
<Panel
key={panelData.id}
title={panelData.title}
content={panelData.content}
/>
)}
</div>
);
}
Now when you look at the above example, you can see we have the animate
class on the h2
element. This is the class that is responsible for triggering the animation. I've seen this situation handled in code bases of all sizes with useEffect()
and setTimeOut()
pulled in, and probably some additional useState()
as well, all being used to update the class name directly when props change. This results in a heck of alot of boilerplate in the component just to re-animate when the props change.
Let's see if you can spot on the difference below which uses the power of keys to achieve the effect we want.
// app.js
function Panel({ title, content }) {
return (
<div className={`panel`}>
<h2 className={`animate`}>{title}</h2>
<p>{content}</p>
</div>
);
}
export default function App() {
const [activePane, setActivePane] = useState(DATA[0].id);
const panelData = DATA.filter((item) => item.id === activePane)[0];
return (
<div className="App">
<ul>
{DATA.map(({ id, title }) => (
<li key={id}>
<button onClick={() => setActivePane(id)}>{title}</button>
</li>
))}
</ul>
{panelData && (
<Panel
key={panelData.id}
title={panelData.title}
content={panelData.content}
/>
)}
</div>
);
}
Just from the single inclusion of key
and the unique id value passed to it, we are telling React to destroy this component when the key changes, as that component no longer exists. This will cause React to recreate the underlying structure which in turns causes the CSS to be triggered in the browser, meaning our ugly animation is reapplied automagically. Lovely!
Check out the below CodeSandbox for a live demo.
The following link from the updated React docs explain this is far better detail than I ever could, and are really worth a read.
Cheerio!
Chris