4

My Goal:

I'm trying to build a component that when you give it props.items and props.fadeEvery, it will act as a text rotator. I eventually want it to fade in an out, but I'm having trouble with my window.setInterval.

Possible Issue:

I'm calling setIndex in the useEffect hook, but is that not good practice? How an I have it iterate through the array items infinitely?

TextFade.tsx

// Imports: Dependencies
import React, { useState, useEffect } from 'react';

// TypeScript Type: Props
interface Props {
  items: Array<string>,
  fadeEvery: number,
};

// Component: Text Fade
const TextFade: React.FC<Props> = (props): JSX.Element => {
  // React Hooks: State
  const [ index, setIndex ] = useState<number>(0);

  // React Hooks: Lifecycle Methods
  useEffect(() => {
    const timeoutID: number = window.setInterval(() => {
      // End Of Array
      if (index > props.items.length) {
        // Set Data
        setIndex(0);
      }
      else {
        // Set Data
        setIndex(index + 1);
      }
    }, props.fadeEvery * 1000);

    // Clear Timeout On Component Unmount
    return () => window.clearTimeout(timeoutID);
  }, []);

  return (
    <div id="text-fade-container">
      <p id="text-fade-text">{props.items[index]}</p>
    </div>
  );
};

// Exports
export default TextFade;
jefelewis
  • 1,850
  • 1
  • 27
  • 63
  • `index` is in what's called a closure. Your use effect has been told to only render once `[]`.. So you need to use the callback version of `setIndex` – Keith Apr 07 '21 at 20:54
  • 1
    Just another heads up, if your using `setInterval`, it's most likely better to `clearInterval` not `clearTimeout`. MDN does say the ID's are shared, but for clarity its best to keep them in sync. – Keith Apr 07 '21 at 21:11
  • Does this answer your question? [State not updating when using React state hook within setInterval](https://stackoverflow.com/questions/53024496/state-not-updating-when-using-react-state-hook-within-setinterval) – Yangshun Tay Dec 17 '22 at 00:42

3 Answers3

4

Your index values are taken from initital closure and it won't update unless useEffect callback is called again. You can instead use functional way to update state

useEffect(() => {
    const timeoutID: number = window.setInterval(() => {
      // End Of Array 
      setIndex(prevIdx => {
         if (prevIdx > props.items.length) {
            // Set Data
             return 0;
         }
         else {
           // Set Data
           return prevIdx + 1;
          }
      })
      
    }, props.fadeEvery * 1000);

    // Clear Timeout On Component Unmount
    return () => window.clearTimeout(timeoutID);
  }, []);
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
2

Below I've knocked up a snippet using the callback version of setState, this avoid the closure issue you get by using useEffect with []..

const {useState, useEffect} = React;


const TextFade = (props) => {
  // React Hooks: State
  const [ index, setIndex ] = useState(0);

  // React Hooks: Lifecycle Methods
  useEffect(() => {
    const timeoutID: number = window.setInterval(() => {
      // End Of Array
      setIndex(index => 
        index + 1 >= props.items.length
          ? 0
          : index + 1);
    }, props.fadeEvery * 1000);

    // Clear Timeout On Component Unmount
    return () => window.clearInterval(timeoutID);
  }, []);

  return (
    <div id="text-fade-container">
      <p id="text-fade-text">{props.items[index]}</p>
    </div>
  );
};



ReactDOM.render(<TextFade items={['one','two', 'three']} fadeEvery={1}/>, document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

<div id="mount"></div>
Keith
  • 22,005
  • 2
  • 27
  • 44
1

As @Keith said:

index is in what's called a closure. Your use effect has been told to only render once [].. So you need to use the callback version of setIndex

So, your useEffect hook will be:

useEffect(() => {
    const timeoutID: number = window.setInterval(() => {
      // End Of Array
      if (index > props.items.length) {
        // Set Data
        setIndex(0);
      } else {
        // Set Data
        setIndex(index + 1);
      }
    }, props.fadeEvery * 1000);

    // Clear Timeout On Component Unmount
    return () => window.clearTimeout(timeoutID);
  }, [index]);

Here is the working demo at CodeSandbox.

Zunayed Shahriar
  • 2,557
  • 2
  • 12
  • 15
  • Don't rely on comments sticking around. They are often removed. – Heretic Monkey Apr 07 '21 at 21:05
  • Ok. Thanks, @HereticMonkey. – Zunayed Shahriar Apr 07 '21 at 21:07
  • 1
    This is using the dependency of `useEffect` rather than the setState callback, and is certainly another option. You could even then change `setInterval` to `setTimeout`. If the useEffect could be time consuming, I would even say this is the better option. – Keith Apr 07 '21 at 21:22