Search code examples
reactjscarouselcodesandboxframer-motion

Component duplicating when using keys


I am attempting to make a component carousel in React using Framer Motion. The children of AnimatePresence need a key, so I decided to pass a page state as the key. However, doing this makes the component I am trying to render duplicate. I thought it was because the key would eventually be re-used, so I made a function that generates a random string to use as the key, but that duplicates the component as well.

Component using carousel

const ProjectList = props => {

  const [page, setPage] = useState(0);

  const projects = [
    <Project 
      name="Example"
      desc="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero nunc consequat interdum varius sit amet."
      image={require("../img/exampleproject.png")}
    />,
    <Project 
      name="Example2"
      desc="Another example. This one does nothing too. What a suprise!"
      image={require("../img/exampleproject.png")}
    />
  ]

  const genKey = () => {
    return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
  }

  const paginate = (newPage) => {
    setPage(newPage)
  }

  return (
    
    <div className="project-list">
      <AnimatePresence>
        <motion.button key={genKey()} onClick={() => paginate(page-1)}>
          <ArrowBackIosIcon/>
        </motion.button>
        <motion.div key={genKey()} className="project-list"
          initial={{opacity: 0}}
          animate={{opacity: 1}}
          exit={{opacity: 0}}
        >
          {projects[page]}
        </motion.div>
        <motion.button key={genKey()} onClick={() => paginate(page+1)}>
          <ArrowForwardIosIcon/>
        </motion.button>
      </AnimatePresence>
    </div>
  );

I'm not sure how to use libraries like Framer Motion in the snippet editor, so I put it on CodeSandbox

Edit priceless-sammet-9w1t3

When I don't use a key, it works as expected, however whenever I click one of the arrow buttons it throws the following warnings

warnings

P.S I know eventually the page value will go out of range of the length of projects, I plan on fixing that once I can get this problem sorted.


Solution

  • Actually you do not need any key here. Adding key (and moreover adding random keys -as you did with the Math.random() method) for elements that react manage will override the react pre-defined keys and could lead your program to kind of rendering bugs as you got here.

    Keys are needed only when you generate components from an array (with the map function for example).

    From the doc :

    Keys help React identify which items have changed, are added, or are removed. Keys should be given to the elements inside the array to give the elements a stable identity:

    Here you do not need that.

    Also a key must be the same for every rendering, and here you generate a random key (from the math random method each time you render this component).

    Now since you are using this library : https://www.framer.com/api/motion/animate-presence/#usage

    It precised that you have to use a key. The problem here is that you must give a constant keys. So remove the Math Random function and give some constant string as I did here :

    import React, { useState } from "react";
    import Section from "./Section.js";
    import { AnimatePresence, motion } from "framer-motion";
    import Project from "./Project.js";
    import ArrowBackIosIcon from "@material-ui/icons/ArrowBackIos";
    import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos";
    
    const ProjectList = props => {
      const [page, setPage] = useState(0);
    
      const projects = [
        <Project
          name="Example"
          desc="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Libero nunc consequat interdum varius sit amet."
          image={
            "https://i.picsum.photos/id/928/600/300.jpg?hmac=ai-33AKRXhJnTcm88ArxRypuNNrfztMdJ-ui_8dhe8c"
          }
        />,
        <Project
          name="Example2"
          desc="Another example. This one does nothing too. What a suprise!"
          image={
            "https://i.picsum.photos/id/258/600/300.jpg?hmac=d-pTq52drP8dj3vsxB72sOgifDUNcookREV33ffONbw"
          }
        />
      ];
    
    
    
      const paginate = newPage => {
        setPage(newPage);
      };
    
      return (
        <div className="project-list">
          <AnimatePresence>
            <motion.button key="img1" onClick={() => paginate(page - 1)}>
              <ArrowBackIosIcon />
            </motion.button>
            <motion.div
              key="img2"
              className="project-list"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
            >
              {projects[page]}
            </motion.div>
            <motion.button key="img3" onClick={() => paginate(page + 1)}>
              <ArrowForwardIosIcon />
            </motion.button>
          </AnimatePresence>
        </div>
      );
    };
    
    export default ProjectList;