0

I'm trying to build a component that is similar to typed.js, in the sense that text (from an array) will be looped over indefinitely. Here is what I have so far of this component (using styled components):

const cdRotateIn = keyframes`
  0% {
    transform: rotateX(180deg);
    opacity: 0;
  }
  35% {
    transform: rotateX(120deg);
    opacity: 0;
  }
  65% {
    opacity: 0;
  }
  100% {
    transform: rotateX(360deg);
    opacity: 1;
  }
`

const cdRotateOut = keyframes`
  0% {
    transform: rotateX(0deg);
    opacity: 1;
  }
  35% {
    transform: rotateX(-40deg);
    opacity: 1;
  }
  65% {
    opacity: 0;
  }
  100% {
    transform: rotateX(180deg);
    opacity: 0;
  }
`

const Wrapper = styled.div``

const Headline = styled.h1``

const StaticText = styled.span``

const AnimatedTextContainer = styled.span`
  display: inline-block;
  perspective: 300px;
`

const AnimatedText = styled.b`
  opacity: 0;
  transform-origin: 50% 100%;
  transform: rotateX(180deg);
  display: inline-block;
  position: absolute;
  left: 0;
  top: 0;

  ${ifProp('isVisible', css`
    position: relative;
    opacity: 1;
    transform: rotateX(0deg);
    animation: ${cdRotateIn} 1.2s;
  `)}

  ${ifProp('isHidden', css`
    transform: rotateX(180deg);
    animation: ${cdRotateOut} 1.2s;
  `)}
`

const FlipAnimation = ({ words }) => {
  const [currentIndex, setCurrentIndex] = useState(0)
  const animationDelay = 1000

  useEffect(() => {
    let loopInterval = setInterval(() => {
      animateHeadline(words)
    }, animationDelay)
    return () => {
      clearInterval(loopInterval)
    }
  }, [])


  const animateHeadline = (words) => {
    setInterval(() => {
      setNextIndex()
    }, animationDelay);
  }

  const setNextIndex = () => {
    if (currentIndex < words.length - 1) {
      setCurrentIndex(currentIndex + 1)
    } else {
      setCurrentIndex(0)
    }
  }

  const animatedTexts = () => words.map((word, index) =>
    <AnimatedText key={index} isVisible={currentIndex === index} isHidden={currentIndex !== index}>{word}</AnimatedText>
  )

  return (
    <Wrapper>
      <Headline>
        <StaticText>My favourite food is</StaticText>
        <AnimatedTextContainer>
          {animatedTexts()}
        </AnimatedTextContainer>
      </Headline>
    </Wrapper>
  )
}
FlipAnimation.defaultProps = {
  words: ['bacon', 'eggs', 'sausage']
}

The issue that I can see here in the debugging console is that currentIndex does not properly get updated on each run of the loop. I can't understand why this is happening, since this code gets run each second:

if (currentIndex < words.length - 1) {
   setCurrentIndex(currentIndex + 1)
} else {
   setCurrentIndex(0)
}

1 Answer 1

1

Your function closes over the currentIndex variable from the very first render. So currentIndex is always 0, and setCurrentIndex(currentIndex + 1) will always try to set it to 1.

One option to fix this is to make sure you create a new function each time, and set a new timeout using that new function. This way, each new function closes over the most recent value. However, there's a simpler approach: setCurrentIndex allows you to pass a function into it, which will get called with whatever the current value happens to be. You can then calculate the new value based on that:

setCurrentIndex(previousVal => {
  if (previousVal < words.length - 1) {
    return previousVal + 1;
  }
  return 0;
});
Sign up to request clarification or add additional context in comments.

2 Comments

great answer! currentIndex is working correctly now. Could you elaborate on what's happening here under the hood? As I understand it, react components are closures, but I'm unsure why this means that currentIndex is always 0
You call setInterval exactly once, and you point it to a function. That function is a closure, which has a const named currentIndex in its scope. The value of that const is 0 and will never change. If the component renders again, you do create new functions which have different values in their scope, but no intervals are started with these functions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.