Search code examples
javascriptarraysreactjsalgorithmuse-effect

How to change places in an Array using React and useEffect?


There's a list of 5 elements. Per default, the list (array) should show ["A", "B", "C", "D", "E"]. Every second the elements should rotate by one position:

List after 1 second: ["B", "C", "D", "E", "A"];

After 2 seconds: ["C", "D", "E", "A", "B"];

After 3 seconds: ["D", "E", "A", "B", "C"];

I'm learning and I need help. How would you solve this problem? This is my incorrect solution:

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

function App() {
  const [term, setTerm] = useState();
  const [letter, setLetter] = useState([]);

  let array = ["A", "B", "C", "D", "E"];

  useEffect(() => {
    const interval = () =>
      setInterval(() => {
        array.push(array.shift());
        setLetter(array);
       }, 1000);

    interval();

    return clearInterval(interval);
  }, [array]);

  return (
    <div className="center">
      <input
        type="text"
        value={term}
        onChange={(e) => setTerm(e.target.value)}
      />
      {letter &&
        letter.map((l, i) => {
          return (
            <ul key={l}>
              <li>{l[0]}</li>
              <li>{l[1]}</li>
              <li>{l[1]}</li>
              <li>{l[3]}</li>
              <li>{l[4]}</li>
            </ul>
          );
        })}
    </div>
  );
}

export default App;

I'm just a learner, I'm stuck, and I need help. It changes rapidly, like every 50ms.

I'll analyze the answer and learn from it because I can't do it by myself.


Solution

  • I would suggest that you avoid modifying your array. Rather than using push and shift, generate a new version of the array by arr => [...arr .slice (1), arr [0]] or using concat:

    const {useState, useEffect} = React;
    
    const App = () => {
      const [letters, setLetters] = useState (["A", "B", "C", "D", "E"])
    
      useEffect(() => {
        const timer = setTimeout (
          () => setLetters (arr => arr .slice (1) .concat (arr [0])), 
          1000
        )
        return () => clearTimeout (timer)
     })
    
      return (<div>{letters .join (' ')}</div>)
    }
    
    ReactDOM .render (<App/>, document .getElementById ("main"))
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
    <div id="main"></div>

    Modifying data only through the setter makes things much more consistent.

    Note that this will work fine without returning the clearTimeout, but it's probably a good habit to be in.