React App becomes blank when I try to update an array in the state

Viewed 130

I am trying to manipulate the images shown in my React App by voice. I implemented the SR, which works fine:

   <button onClick={SpeechRecognition.startListening}>Start</button>

I have an array of loadImages in my state, which is empty first:

  const [loadImages, setLoadImages] = React.useState([]);

Whenever the word spoken is "kitten", the array of loadImages gets updated in this way:

  if(transcript == "kitten")
  {
    const newImages = loadImages.concat({image: 'https://www.onlinekittencare.com/wp-content/uploads/2020/07/vChK6pTy3vN3KbYZ7UU7k3-1200-80.jpg'})
    setLoadImages(newImages);
  }

The transcript you see is a variable, which I initialized here:

  const {transcript} = useSpeechRecognition();

In the render I use it to show what the SR understood, so if I say "hello" it shows "hello":

   <p id="transcript">Transcript: {transcript}</p>

And this is where the images in loadImages show up:

 {images.map((image) => {
              return <URLImage image={image}/>;
            })}

The problem is that whenever I say "kitten", which as stated above is used as a command to add the picture to the array loadImages, my React App gets blank. In the inspect I can also see that it says react-dom.development.js:14997 Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

How do I fix that?

EDIT (I added the whole code):

function App() {

  const [rectangles, setRectangles] = useState([]);
  const [circles, setCircles] = useState([]);
  const [selectedId, selectShape] = useState(null);
  const [shapes, setShapes] = useState([]);
  const [, updateState] = React.useState();
  const stageEl = React.createRef();
  const layerEl = React.createRef();
  const fileUploadEl = React.createRef();
  const [brushSize, setBrushSize] = React.useState('5');
  const [isDrawing, setIsDrawing] = React.useState(false);
  const dragUrl = React.useRef();
  //const stageRef = React.useRef();
  const [images, setImages] = React.useState([]);
  const [loadImages, setLoadImages] = React.useState([]);
  const getRandomInt = max => {
    return Math.floor(Math.random() * Math.floor(max));
  };

  const {transcript} = useSpeechRecognition();

  const URLImage = ({image}) => {
    const [img] = useImage(image.src);
    return (
      <Image
        image = {img}
        x = {image.x}
        y = {image.y}

        offsetX = {50}
        offsetY = {50}

        width={200}
        height={200}
        draggable
      />
    );
  };

  const drawLine = () => {
    setIsDrawing(true);
    if(isDrawing){
      addLine(stageEl.current.getStage(), layerEl.current, brushSize);
    };
    
  };

  const eraseLine = () => {
    addLine(stageEl.current.getStage(), layerEl.current, brushSize, "erase");
  };
  const addRectangle = () => {
    setIsDrawing(false);
    const rect = {
      x: getRandomInt(100),
      y: getRandomInt(100),
      width: 100,
      height: 100,
      fill: "red",
      id: `rect${rectangles.length + 1}`,
    };
    const rects = rectangles.concat([rect]);
    setRectangles(rects);
    const shs = shapes.concat([`rect${rectangles.length + 1}`]);
    setShapes(shs);

  };

  const forceUpdate = React.useCallback(() => updateState({}), []);
  
  const undo = () => {
    const lastId = shapes[shapes.length - 1];
    let index = circles.findIndex(c => c.id == lastId);
    if (index != -1) {
      circles.splice(index, 1);
      setCircles(circles);
    }
    index = rectangles.findIndex(r => r.id == lastId);
    if (index != -1) {
      rectangles.splice(index, 1);
      setRectangles(rectangles);
    }
    index = images.findIndex(r => r.id == lastId);
    if (index != -1) {
      images.splice(index, 1);
      setImages(images);
    }
    shapes.pop();
    setShapes(shapes);
    forceUpdate();
  };

  document.addEventListener("keydown", ev => {
    if (ev.code == "Delete") {
      let index = circles.findIndex(c => c.id == selectedId);
      if (index != -1) {
        circles.splice(index, 1);
        setCircles(circles);
      }
      index = rectangles.findIndex(r => r.id == selectedId);
      if (index != -1) {
        rectangles.splice(index, 1);
        setRectangles(rectangles);
      }
      index = images.findIndex(r => r.id == selectedId);
      if (index != -1) {
        images.splice(index, 1);
        setImages(images);
      }
      forceUpdate();
    }
  });

  if(transcript == "kitten")
  {
    const newImages = loadImages.concat({image: 'https://www.onlinekittencare.com/wp-content/uploads/2020/07/vChK6pTy3vN3KbYZ7UU7k3-1200-80.jpg'})
    setLoadImages(newImages);
  }

  return (
<div className="home-page">

{loadImages.map(image => (
  <img id="img" className="img"
  src={image.image}
  width="200"
  height="200"
  onDragStart={(e) => {
    dragUrl.current = e.target.src;}}
/>
))}
    
    <div
        onDrop={(e) => {
          e.preventDefault();
          // register event position
          stageEl.current.setPointersPositions(e);
          // add image
          setImages(
            images.concat([
              {
                ...stageEl.current.getPointerPosition(),
                src: dragUrl.current,
              },
            ])
          );
        }}
        onDragOver={(e) => 
          e.preventDefault()
        }
      >
    
      <h1>Whiteboard</h1>

        <button onClick={addRectangle}>
          Rectangle
        </button>
        <button>
          Circle
        </button>
        <button onClick={drawLine}>
          Line
        </button>
        <button onClick={eraseLine}>
          Erase
        </button>
        <select
        value={brushSize}
        onChange={(e) => {
          setBrushSize(e.target.value);
          drawLine();
        }}
      >
        <option value="5">5</option>
        <option value="20">20</option>
        <option value="50">50</option>
      </select>
        <button variant="secondary">
          Text
        </button>
        <button variant="secondary">
          Image
        </button>
        <button variant="secondary" onClick={undo}>
          Undo
        </button>
       <p id="transcript">Transcript: {transcript}</p>
       <button onClick={SpeechRecognition.startListening}>Start</button>
      <Stage
        width={window.innerWidth * 0.9}
        height={window.innerHeight - 150}
        ref={stageEl}
        dragabble
        onMouseDown={e => {
          // deselect when clicked on empty area
          const clickedOnEmpty = e.target === e.target.getStage();
          if (clickedOnEmpty) {
            selectShape(null);
          }
        }}
      >
        <Layer ref={layerEl}>
        {rectangles.map((rect, i) => {
            return (
              <Rectangle
                key={i}
                shapeProps={rect}
                isSelected={rect.id === selectedId}
                //onSelect={() => {
                //  selectShape(rect.id);
                //}}
                onChange={newAttrs => {
                  const rects = rectangles.slice();
                  rects[i] = newAttrs;
                  setRectangles(rects);
                }}
              />
            );
          })}
         
         {images.map((image) => {
              return <URLImage image={image}/>;
            })}

        </Layer>
      </Stage>
      </div>
    </div>
  );
}
export default App;
1 Answers

Based on the code you've shared, it has to do with how you're updating the state if the transcript is equal to kitten.

Essentially, the logic you've written says, on render, if the transcript is kitten, update the state. BUT, when you update the state, that will re-render, and hit that logic again... and again... and again. The solution here is to wrap that in a useEffect – React Docs explain it best but in simple terms, you want to "do something" as a side effect of "something else".

In this case, if the transcript updates, you want to check the state of transcript, and if it meets a condition, you want to update your state:

React.useEffect(() => {
  if (transcript === "kitten") {
    const newImages = loadImages.concat({image: 'https://www.onlinekittencare.com/wp-content/uploads/2020/07/vChK6pTy3vN3KbYZ7UU7k3-1200-80.jpg'})
    setLoadImages(newImages);
  }
}, [transcript]);

The final piece to the useEffect is a dependency array ([transcript])–this dictates to React which item you want to watch for changes in–if transcript changes, it will run your effect and only when it changes, instead of every time it renders.

Related