February 22, 2023

The Power of Keys in Framer Motion

Exploring how to use the React key prop to power your Framer Motion animations.

What do these animations have in common?

Well, they're both written in Framer Motion, but more importantly, they both take advantage of the React key prop. That's right — the same key prop that you use to suppress the React warning when writing loops:

["a", "b", "c"].map((item) => <div key={item}>{item}</div>);
["a", "b", "c"].map((item) => <div key={item}>{item}</div>);

You see, the key prop is a lot more powerful than it seems, and in this post, we'll explore exactly what it is and how you can use it to create some pretty cool animations.

A Primer on Keys

The purpose of the key prop is to uniquely identify a React component. If React sees that a component's key has changed between renders, it will unmount that component and mount a new one in its place.

For the purposes of this post, I'm using the term "React component" to refer to both custom components (like const MyComponent = (props) => { ... }) and JSX tags (like <div />).

The implication of this is we can use keys to explicitly tell React to "re-mount" a component.

A Counter Component

Consider this little counter component:

const Counter = ({ name }) => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{name}</p>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increment
      </button>
    </div>
  );
};
const Counter = ({ name }) => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{name}</p>
      <h1>{count}</h1>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        Increment
      </button>
    </div>
  );
};

Here, we render the component with the name "John". Try incrementing the counter a couple of times and then click on the "Change Name" button:

const [name, setName] = useState("John");
return (
  <div>
    <Counter name={name} />
    <button
      onClick={() => {
        setName(name === "Jane" ? "John" : "Jane");
      }}
    >
      Change Name
    </button>
  </div>
);
const [name, setName] = useState("John");
return (
  <div>
    <Counter name={name} />
    <button
      onClick={() => {
        setName(name === "Jane" ? "John" : "Jane");
      }}
    >
      Change Name
    </button>
  </div>
);

John

0

Notice that changing the name maintains the counter's value. Most of the time, this is what you want — imagine if the component's state resets every time one of its props changes!

What if you do want to reset the state, though? That's where keys come in. This time, we'll add a key prop to <Counter /> and set it to name. Now try clicking on "Increment" then "Change Name":

const [name, setName] = useState("John");
return (
  <div>
    <Counter name={name} key={name} />
    <button
      onClick={() => {
        setName(name === "Jane" ? "John" : "Jane");
      }}
    >
      Change Name
    </button>
  </div>
);
const [name, setName] = useState("John");
return (
  <div>
    <Counter name={name} key={name} />
    <button
      onClick={() => {
        setName(name === "Jane" ? "John" : "Jane");
      }}
    >
      Change Name
    </button>
  </div>
);

John

0

This time, clicking on "Change Name" resets the counter to 0!

This state reset is just a byproduct, however; under the hood, React is unmounting the old instance of <Counter /> and mounting a new one in its place.

Short Quiz

Let's take a short quiz. Take a look at the following code and try to answer the question:

What does the console look like after "Change Key" is pressed?

const MyComponent = () => {
  React.useEffect(() => {
    console.log("mounted");
    return () => console.log("unmounted");
  }, []); // <-- no dependencies!

  return <p>Hello World</p>;
};

const App = () => {
  const [key, setKey] = React.useState(0);
  return (
    <>
      <button onClick={() => setKey(key + 1)}>Change Key</button>
      <MyComponent />
    </>
  );
};
const MyComponent = () => {
  React.useEffect(() => {
    console.log("mounted");
    return () => console.log("unmounted");
  }, []); // <-- no dependencies!

  return <p>Hello World</p>;
};

const App = () => {
  const [key, setKey] = React.useState(0);
  return (
    <>
      <button onClick={() => setKey(key + 1)}>Change Key</button>
      <MyComponent />
    </>
  );
};

Now let's change the key of the component when we click on "Change Key":

What does the console look like after "Change Key" is pressed?

const MyComponent = () => {
  React.useEffect(() => {
    console.log("mounted");
    return () => console.log("unmounted");
  }, []); // <-- no dependencies!

  return <p>Hello World</p>;
};

const App = () => {
  const [key, setKey] = React.useState(0);
  return (
    <>
      <button onClick={() => setKey(key + 1)}>Change Key</button>
      <MyComponent key={key} />
    </>
  );
};
const MyComponent = () => {
  React.useEffect(() => {
    console.log("mounted");
    return () => console.log("unmounted");
  }, []); // <-- no dependencies!

  return <p>Hello World</p>;
};

const App = () => {
  const [key, setKey] = React.useState(0);
  return (
    <>
      <button onClick={() => setKey(key + 1)}>Change Key</button>
      <MyComponent key={key} />
    </>
  );
};

But how is this useful to Framer Motion?

In Framer Motion, we can make mount animations using the animate prop and unmount animations using AnimatePresence. Since changing keys lets us re-mount components, we can essentially use keys to trigger animations!

Let's take a look at a few examples.

Refresh Component

One utility component I use all the time is this refresh component:

import React from 'react'

export const Refresh = ({ children }) => {
  const [key, setKey] = React.useState(0);
  return (
    <>
      <button onClick={() => setKey(key + 1)}>Refresh</button>
      <div key={key}>{children}</div>
    </>
  );
}

When the refresh button is clicked, the component remounts, causing the mount animation to play again. I've found this to be super handy when working on a component's mount animation!

Again, this works because we change the key of the <div /> element, thereby telling React to unmount the existing component and mount a new instance.

Animating Text Changes

Of course, this technique is useful outside of development as well.

In the Japanese app I'm working on, I have this button that changes its text content when the user submits an answer:

Using what we know about keys, how do you think we should implement this?

import React from 'react'
import { motion } from 'framer-motion'

export function NextButton() {
  const [toggled, toggle] = React.useReducer((state) => !state, false);

  return (
    <motion.button layout id="motion" onClick={toggle}>
      <motion.span
        animate={{ opacity: 1 }}
        initial={{ opacity: 0 }}
        transition={{ delay: 0.2 }}
      >
        {toggled ? "Next" : "Show Answer"}
      </motion.span>
    </motion.button>
  )
}

By adding a key to the <motion.span /> element!

<motion.span key={toggled ? "done" : "ready"}>...</motion.span>
<motion.span key={toggled ? "done" : "ready"}>...</motion.span>

When we toggle the state, we simultaneously change the key of the <motion.span /> element, causing it to replay its mount animation.

Here's another animation I'm especially fond of that uses keys in conjunction with AnimatePresence:

What's really cool about this animation is how little code you need to implement it. Here it is in its entirety:

<div style={{ position: "relative", overflow: "hidden" }}>
  <AnimatePresence mode="popLayout">
    <motion.div
      key={word}
      initial={{ x: -300 }}
      animate={{ x: 0 }}
      exit={{ x: 300 }}
    >
      {word}
    </motion.div>
  </AnimatePresence>
</div>
<div style={{ position: "relative", overflow: "hidden" }}>
  <AnimatePresence mode="popLayout">
    <motion.div
      key={word}
      initial={{ x: -300 }}
      animate={{ x: 0 }}
      exit={{ x: 300 }}
    >
      {word}
    </motion.div>
  </AnimatePresence>
</div>

The key in this case is serving a double purpose:

  1. First, it tells React to remount the component when the word changes;
  2. Second, it tells AnimatePresence that the child has changed, triggering the exit animation of the old word and the mount animation of the new word.

Another Solution

Now you don't technically need keys to implement this. If you know the number of items in your carousel ahead of time and you don't want your items to loop, you could technically line them all up and slide them across:

<motion.div
  animate={{ x: -600 + currentIndex * 300 }}
  style={{ display: "flex" }}
>
  {KANJI.map((char) => (
    <p key={char} style={{ width: 300, flexShrink: 0 }}>
      {char}
    </p>
  ))}
</Box>
<motion.div
  animate={{ x: -600 + currentIndex * 300 }}
  style={{ display: "flex" }}
>
  {KANJI.map((char) => (
    <p key={char} style={{ width: 300, flexShrink: 0 }}>
      {char}
    </p>
  ))}
</Box>

As with everything in computer science, there are multiple ways to solve a problem! Use the one that fits your use case the best.

Summary

To summarize, the key prop is a special prop in React that lets you uniquely identify components. When a component's key changes, React will treat that component as a different component, unmounting the existing component.

In Framer Motion, we can exploit this behavior to trigger animations. For example, we can use keys to trigger mount animations by changing the key of the component we want to animate.

That's all for today; thanks for reading!