javascriptreactjstypescriptframer-motion

Animation resets when InView component callback is called


Whenever I try and update state in my InView component, it brings back my AnimationText to the initial state it was in until I refresh. This is only when I update my darkNavbar state to inView. I'm wondering if this has to do with a rerender screwing up framer motion maybe?

app/page.tsx:

'use client';
import { useCallback, useEffect, useState } from 'react';
import styles from './page.module.scss';
import global from '@/scss/global.module.scss';
import { Container, Navbar, AnimatedText, Globe } from '@/components';
import { useMotionTimeline } from '@/hooks';
import { motion } from 'framer-motion';
import { timeline } from './timeline';
import cx from 'classnames';
import { InView } from 'react-intersection-observer';

const Home = () => {
  const heroScope = useMotionTimeline(timeline);
  const [darkNavbar, setDarkNavbar] = useState(false);

  return (
    <>
      <motion.div ref={heroScope} className={styles.heroContainer}>
        <Navbar />
        <Container>
          <section className={styles.heroSection}>
            <div className={styles.globeContainer}>
              <motion.div className={styles.globeBox} id="globe-box" initial={{ height: 0 }}>
                <AnimatedText
                  className={cx(global.heading, styles.heroTitle)}
                  id="hero-text"
                  auto={false}
                >
                  Optimizing your brand to maximize your impact
                </AnimatedText>
              </motion.div>
              <Globe id="globe" />
            </div>
          </section>
        </Container>
      </motion.div>
      <div className={styles.afterHero}>
        <Container className={styles.oneLineContainer}>
          <AnimatedText as="h2" className={cx(global.subheading, styles.oneLine)}>
            We’re giving the renewable energy space a makeover.
          </AnimatedText>
        </Container>

        <InView as="div" className={styles.expertiseContainer} onChange={(inView) => setDarkNavbar(inView)}></InView>
      </div>
    </>
  );
};

export default Home;

components/AnimatedText.tsx:

'use client';
import React, { useEffect } from 'react';
import { motion, useAnimate, stagger } from 'framer-motion';
import { useInView } from 'react-intersection-observer';
import styles from './AnimatedText.module.scss';

type TextElements =
  | 'p'
  | 'span'
  | 'a'
  | 'strong'
  | 'em'
  | 'blockquote'
  | 'h1'
  | 'h2'
  | 'h3'
  | 'h4'
  | 'h5'
  | 'h6';

interface ComposeTagProps {
  children: React.ReactNode;
}

export interface AnimatedTextProps {
  as?: TextElements;
  className?: string;
  children: string;
  auto?: boolean;
  id?: string;
}

export const AnimatedText = ({
  as = 'h1',
  className,
  children,
  auto = true,
  id,
}: AnimatedTextProps) => {
  const textArr = children.split(' ');

  const ComposeTag = ({ children }: ComposeTagProps) => {
    return React.createElement(as, { className, id }, children);
  };

  const ComposeMotion = motion(ComposeTag);

  const { ref, inView } = useInView({ triggerOnce: true });

  const [scope, animate] = useAnimate();

  useEffect(() => {
    if (auto && inView) {
      animate(
        'span',
        {
          opacity: 1,
          y: 0,
        },
        {
          delay: stagger(0.0185),
          ease: 'linear',
        },
      );
    }
  }, [inView, auto, animate]);

  return (
    <div ref={scope}>
      <div ref={ref}>
        <ComposeMotion>
          {textArr.map((word, idx) => (
            <motion.span
              key={`${word}-${idx}`}
              className={styles.word}
              initial={{ opacity: 0, y: 8 }}
            >
              {idx === textArr.length - 1 ? word : `${word} `}
            </motion.span>
          ))}
        </ComposeMotion>
      </div>
    </div>
  );
};

export default AnimatedText;

Solution

  • Ok, I was able to fix this by applying useCallback and useMemo as follows:

    const ComposeTag = useCallback(
      ({ children }: ComposeTagProps) => {
        return React.createElement(as, { className, id }, children);
      },
      [as, className, id],
    );
    
    const ComposeMotion = useMemo(() => motion(ComposeTag), [ComposeTag]);