Milán Nikolics
Milán Nikolics

Reputation: 621

StaggerChildren work only first render at text animation

I try make an text Animation with framer motion, React

The Animation work fine still.

Since I use text Animation after switch route. So after change path.

But will be lot of components.

So I try solving things inside the component.

Not animation when path change but when the props change.

Now the Animation also work but not perfect

The essence:

The First Animation Fine with "A A"

The second and third Animation not appear one by one but at once

codesandbox

I guess based on other post something problem with stagger children. Bit I dont know exatly what should i do that working text animation one by one when props change

More simple if I show you on codesandbox:

Data.js

const data = [
  {
    id: 0,
    maintext: "A A",
    answerButtons: [{ answerText: "Next", isCorrect: true }],
    image: "",
  },
  {
    id: 1,
    maintext: "B B B B",
    answerButtons: [{ answerText: "Next", isCorrect: true }],
  },
  {
    id: 2,
    maintext: "C C C C C C",
    answerButtons: [{ answerText: "Next", isCorrect: true }],
  },
];

export default data;

MobilBase.js

import React, { useState, useEffect } from "react";
import SpeechBubble from "../../../components/SpeechBubble/SpeechBubble";
import Data from "../../../utils/data";
import "./MobilBase.css";

const MobilBase = () => {
  const [counter, setCounter] = useState(0);
  const [data, setData] = useState(Data);

  const nextHandler = (i) => {
    if (Data[counter].answerButtons[i].isCorrect === true) {
      setCounter((prevState) => {
        return prevState + 1;
      });
    }
  };

  return (
    <div className="base base-page-grid">
      <div className="counter">
        <h2>
          {counter} / {data.length - 1}
        </h2>
      </div>

      <div className="speech-bubble-outer">
        <SpeechBubble maintext={data[counter].maintext} />
      </div>
      {data[counter].answerButtons.map((answerbutton, i) => (
        <div key={i} className="base-buttons-outer">
          <div>
            <button
              onClick={() => {
                nextHandler(i);
              }}
            >
              <strong>{answerbutton.answerText}</strong>
            </button>
          </div>
        </div>
      ))}
    </div>
  );
};

export default MobilBase;

SpeechBubble.js


import React, { useState, useEffect } from "react";
import Animation from "./Animation";

export default function SpeechBubble(props) {

  return (
    <div className="frame-speech-bubble">
      <div className="zumzum-animation-grid-frame">
        <Animation maintext={props.maintext} />
      </div>
    </div>
  );
}

Animation.js

import React, { useState, useEffect } from "react";
import { motion } from "framer-motion/dist/framer-motion";
import AnimatedText from "./AnimatedText";
import "./Animate.css";

export default function Animation(props) {
  // Placeholder text data, as if from API
  const placeholderText = [{ type: "heading1", text: props.maintext }];

  const container = {
    visible: {
      transition: {
        staggerChildren: 0.025,
      },
    },
  };

  return (
    <motion.div
      className="animation-frame"
      initial="hidden"
      animate="visible"
      variants={container}
    >
      <div className="container-animated-text">
        {placeholderText.map((item, index) => {
          return <AnimatedText {...item} key={index} />;
        })}
      </div>
    </motion.div>
  );
}

AnimatedText.js

import React, { useState, useEffect } from "react";
import { motion } from "framer-motion/dist/framer-motion";

// Word wrapper
const Wrapper = (props) => {
  // We'll do this to prevent wrapping of words using CSS

  return <span className="word-wrapper">{props.children}</span>;
};

// Map API "type" vaules to JSX tag names
const tagMap = {
  paragraph: "p",
  heading1: "h1",
  heading2: "p"
};

// AnimatedCharacters
// Handles the deconstruction of each word and character to setup for the
// individual character animations
const AnimatedCharacters = (props) => {
  // Framer Motion variant object, for controlling animation
  const item = {
    hidden: {
      x: "-200%",
      color: "#0055FF",
      transition: { ease: [0.455, 0.03, 0.515, 0.955], duration: 0.85 }
    },
    visible: {
      x: 0,
      color: "red",
      transition: { ease: [0.455, 0.03, 0.515, 0.955] }
    }
  };

  //  Split each word of props.text into an array

  const splitWords = props.text.split(" ");

  // Create storage array
  let words = [];

  // Push each word into words array

  for (const item of splitWords) {
    words.push(item.split(""));
  }

  // Add a space ("\u00A0") to the end of each word
  words.map((word) => {
    return word.push("\u00A0");
  });

  // Get the tag name from tagMap
  const Tag = tagMap[props.type];
 
  return (
    <Tag>
      {words.map((word, index) => {
        console.log(`return: word ${word} index: ${index}`);
        return (
          // Wrap each word in the Wrapper component
          <Wrapper key={word.id}>
            {words[index].flat().map((element, index) => {
              return (
                <span
                  style={{
                    overflow: "hidden",
                    display: "inline-block"
                  }}
                  key={word.id}
                >
                  <motion.span
                    className="animatedtext spell"
                    style={{ display: "inline-block" }}
                    variants={item}
                  >
                    {element}
                  </motion.span>
                </span>
              );
            })}
          </Wrapper>
        );
      })}
    </Tag>
  );
};

export default AnimatedCharacters;

Upvotes: 1

Views: 932

Answers (1)

Cadin
Cadin

Reputation: 4649

You should not use the loop index as the key in the elements output from map. Since the indices (and thus the keys) will always be the same, it doesn't let Framer track when elements have been added or removed in order to animate them.

Instead use a value like a unique id property for each element. This way React (and Framer) will know when it's rendering a different element (vs the same element with different data). This is what triggers the animations.

Here's a more thorough explanation:
react key props and why you shouldn’t be using index

Upvotes: 2

Related Questions