React parent couldn't update a child component in a mapped JSX list

Viewed 62

NOTE: You can view and edit the code in CodeSandbox.

I have the following parent file which creates a useState list of child component called ProgressBar:

import React, { useState } from 'react';
import ProgressBar from './ProgressBar';
import './App.css';

var idCounter = 0;

export default function App() {
  const [barsArray, setBarsArray] = useState([]);
  const [input, setInput] = useState('');

  function add() {
    setBarsArray((prev) => [
      ...prev,
      <ProgressBar key={idCounter++} restart={false} />,
    ]);
  }

  function remove() {
    setBarsArray((prev) => prev.filter((bar) => bar.key !== input));
  }

  function reset() {
    setBarsArray((prev) =>
      prev.map((bar) => (bar.key === input ? { ...bar, restart: true } : bar))
    );
  }

  return (
    <div className='App'>
      <div className='buttons'>
        <button className='button-add' onClick={add}>
          Add
        </button>
        <button className='button-delete' onClick={remove}>
          Delete
        </button>
        <button className='button-delete' onClick={reset}>
          Reset
        </button>
        <input
          type='number'
          value={input}
          onInput={(e) => setInput(e.target.value)}
        />
      </div>
      <div className='bars-container'>
        {barsArray.map((bar) => (
          <div className='bars-index' key={bar.key}>
            {bar}
            <p>{bar.key}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

The file of the child ProgressBar has the following content:

import React, { useEffect, useState } from 'react';
import './ProgressBar.css';

export default function ProgressBar(props) {
  const [progress, setProgress] = useState(0);

  let interval;

  useEffect(() => {
    interval = setInterval(() => {
      setProgress((prev) => prev + 1);
    }, RnadInt(10, 120));
  }, []);

  useEffect(() => {
    if (progress >= 100) clearInterval(interval);
  }, [progress]);

  if (props.restart === true) {
    setProgress(0);
  }
  return (
    <>
      <div className='ProgressBarContainer'>
        <div className='ProgressBar' style={{ width: progress + '%' }}></div>
      </div>
    </>
  );
}

function RnadInt(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

My problem is that the reset button in the parent doesn't work, as far as I'm concerned, if you change the passed props to the child, the child automatically re-renders, but even though I'm updating the props in reset function in the parent, which maps the old array of child components to a new array and only changes the props of the selected child.

Thanks!

2 Answers

Adding the element in state via add would require to keep the ref of element instead of actual prop bind to element. Suggestion here to use the model object and while rendering use the JSX element.

Please use the below code which defines the barsArray as object state and later uses it render ProgressBar component (from map call).

Check the working codesandbox - https://codesandbox.io/s/admiring-glitter-j3b7sw?file=/src/App.js:0-1446

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

var idCounter = 0;

export default function App() {
  const [barsArray, setBarsArray] = useState([]);
  const [input, setInput] = useState("");

  function add() {
    setBarsArray((prev) => [...prev, { id: idCounter++, restart: false }]);
  }

  function remove() {
    setBarsArray((prev) =>
      prev.filter((bar) => bar.id.toString() !== input.toString())
    );
  }

  function reset() {
    setBarsArray((prev) =>
      prev.map((bar) => {
        return bar.id.toString() === input.toString()
          ? { ...bar, restart: true }
          : { ...bar };
      })
    );
  }

  return (
    <div className="App">
      <div className="buttons">
        <button className="button-add" onClick={add}>
          Add
        </button>
        <button className="button-delete" onClick={remove}>
          Delete
        </button>
        <button className="button-delete" onClick={reset}>
          Reset
        </button>
        <input
          type="number"
          value={input}
          onInput={(e) => setInput(e.target.value)}
        />
      </div>
      <div className="bars-container">
        {barsArray.map((bar) => (
          <div className="bars-index" key={bar.id}>
            <ProgressBar key={bar.id} restart={bar.restart} />
            <p>{bar.key}</p>
          </div>
        ))}
      </div>
    </div>
  );
}
Related