Accessing a dynamically created set of elements in react

Viewed 434

I've made this in straight JS and I'm trying to implement the same sort of thing on a Gatsby site by translating it into React. Basically, the point is to have the letters appear in a random order but in their proper places.

The point at which I'm stuck (I'm pretty new to React) is how to create and access the set of elements (spans) in the randomized order to be able to get them to show in that order.

class about extends Component {

    constructor(props) {
        super(props);
        this.state = {
            aboutText: "This is a test of the emergency broadcast system",
            aboutElements: []
        }
    }
    
    shuffleArray(array) {
        for (var i = array.length - 1; i > 0; i--) {
            var j = Math.floor(Math.random() * (i + 1));
            var temp = array[i];
            array[i] = array[j];
            array[j] = temp;
        }
        return array;
    }

    render() {

        const arrLetters = this.state.aboutText.split("");
        let arrNumbers = Array.from(Array(arrLetters.length).keys());
        arrNumbers = this.shuffleArray(arrNumbers);
        const arrSpans = arrLetters.map((letter, index) => <span key={index} id={"letter" + index}>{letter}</span>);
        console.log(arrSpans)

this.setState({ aboutElements: arrSpans })

        arrNumbers.forEach(function(item, index) {
            let targetSpan = arrSpans.filter( el => el.key == item);
// NEED TO MAKE THE LETTER VISIBLE HERE
 
        });


        return (
        <section id="about" className={styles.about}>
            <div className="container">
                <p>{ this.state.aboutElements }</p>
            </div>
        </section>
    )
    }
    
}

export default about

So what I'm creating the array of letters, turn it into an array of elements, but the moment I try to put it into a state I get this error:

Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

What am I doing wrong?

1 Answers

The problem is that you are calling this.setState({ aboutElements: arrSpans }) inside the render method. The render function is called each time the state changes so the snake bites its tail here, creating an endless loop of render -> setState -> render -> setState -> ...

But the good news is that you can just use a local variable to render elements, without using the state:

render() {

        const arrLetters = this.state.aboutText.split("");

        const arrSpans = arrLetters.map((letter, index) => <span key={index} id={"letter" + index}>{letter}</span>);
        console.log(arrSpans)

// this.setState({ aboutElements: arrSpans }) // <- not needed


        return (
        <section id="about" className={styles.about}>
            <div className="container">
                <p>{ arrSpans }</p>
            </div>
        </section>
    )
    }

I am not sure what you're planning with arrNumbers but i guess you could solve this issue yourself, knowing that you can just use a local variable

UPDATE:

I just made a quick sketch of the desired effect here.

This is a copy of the code:

import React, { useEffect, useState, useRef } from "react";

export default function AnimatedLetters({ text }) {
  const interval = useRef();
  const [elements, setElements] = useState([]);
  useEffect(() => {
    let count = 0;
    if (interval.current) {
      clearInterval(interval.current);
    }
    const indices = shuffleArray(
      Array.from({ length: text.length }, (_, i) => i)
    );
    interval.current = setInterval(() => {
      ++count;
      const chars = text.split("").map((char, i) => (
        <span key={i} style={{ opacity: indices[i] < count ? 1 : 0 }}>
          {char}
        </span>
      ));

      setElements(chars);
      if (count > text.length) {
        clearInterval(interval.current);
      }
    }, 100);
  }, [text]);
  return <p>{elements}</p>;
}

function shuffleArray(array) {
  for (var i = array.length - 1; i > 0; i--) {
    var j = Math.floor(Math.random() * (i + 1));
    var temp = array[i];
    array[i] = array[j];
    array[j] = temp;
  }
  return array;
}

Example usage:

export default function App() {
  const [text, setText] = useState("Hello Codesandbox");
  return (
    <div className="App">
      <AnimatedLetters text={text} />
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
      />
    </div>
  );
}

UPDATE: now it is possible to change the text dynamically

Related