How do you incrementally update a list in React?

Viewed 229

I posted an earlier question about this, but have not had any luck finding a solution, and seeing behavior I don't understand. I've reworked the code a bit and will explain some of the things I tried.

The problem I want to display a list of prime numbers as they are generated. First of all, here's a test function that behaves correctly:

const testPrimes = (primes, setPrimes) => {
  const tPrimes = [3, 5, 7];

  for (const [i, p] of tPrimes.entries()) {
    setTimeout(() => {
      setPrimes((oldPrimes) =>
        oldPrimes.concat(<Response {...{ prime: p, key: p }} />)
      );
    }, i * 1000);
  }
};

And this is called like so:


export default ({ args, appState }) => {
  const [primes, setPrimes] = useState([]);

  useEffect(() => {
    testPrimes(setPrimes);
  }, []);

  return (
    <div>
      <div className="row">{primes}</div>
    </div>
  );
};

This displays each prime in the list at one second intervals, which is the desired behavior.

In the "real" program, I use a generator function instead of an array:


const testPrimes3 = (args, setPrimes) => {
  const gen = fPrimes({ args });

  let i = 0;
  for (const p of gen) {
    console.log("foo");
    setTimeout(() => {
      setPrimes((oldPrimes) =>
        oldPrimes.concat(<Response {...{ prime: p, key: p }} />)
      );
    }, 100);
  }
};

The testPrime3 function is called similar to testPrime above (see below);

When testPrime3 is called, I see 'foo' displayed 3 times in the console, with time intervals between each, but the list in React gets populated all at once when the generator completes, instead of incrementally for each prime yielded.

I've tried wrapping the setEffect call to the generator in a setTimeout, but that has no effect; here's the call with the setTimeout commented out:

  useEffect(() => {
    // setTimeout(() =>
    testPrimes3(args, setPrimes);
    // , 1000);
  }, []);

Is there a better way to control the incremental rendering of elements in the list other than through a setState call?


update It was suggested to not add an element to primes, but render it in the component instead:

const testPrimes3 = (args, setPrimes) => {
  const gen = fPrimes({ args });

  let i = 0;
  for (const p of gen) {
    console.log("foo");
    setTimeout(() => {
      setPrimes((oldPrimes) => oldPrimes.concat(p));
    }, 100);
  }
};

[...]
  return (
    <div>
      <div className="row">
        {primes.map((p) => (
          <Response {...{ prime: p, key: p }} />
        ))}
      </div>
    </div>
  );

This has no effect.

2 Answers

Here is a quick snippet illustrating basic timing in a useEffect including cleaning up on return and passing a dependency array. It stores an index as a ref which is used to adjust the timing as you had been, and to access elements of the primesArr incrementally. Finally, it renders the updated state using a map() call in the component's return.

function App() {
  const [primes, setPrimes] = React.useState([]);
  const primesArr = [2, 3, 5, 7, 11];

  const index = React.useRef(0);

  React.useEffect(() => {
    let i = index.current;
    if (i < primesArr.length) {
      const timer = setTimeout(() =>
        setPrimes(prevPrimes => [...prevPrimes, primesArr[i]]), 1000 * i);
      index.current = i + 1;

      return () => clearTimeout(timer);
    }
  }, [primes]);

  return (
    <div className="App">
      <div>
        <ul>
          {primes.map(prime =>
            <li key={prime}>{prime}</li>
          )}
        </ul>
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>

<div id="root"></div>

And here is a codesandbox using this same timing with a generator (from this question: Javascript generator to generate prime numbers less than a given number)

// https://stackoverflow.com/questions/57057998/javascript-generator-to-generate-prime-numbers-less-than-a-given-number/57058901
function* genPrime(n) {
  if (isNaN(n) || !isFinite(n) || n % 1 || n < 2)
    return " Number not valid : " + n;

  for (var i = 2; i < n; i++) {
    if (isPrime(i)) {
      yield i;
    }
  }
}
function isPrime(num) {
  for (var i = 2; i < num; i++) {
    if (num % i === 0) {
      return false;
    }
  }
  return true;
}

function App() {
  const [primes, setPrimes] = React.useState([]);

  const primeGen = React.useRef(genPrime(100));

  React.useEffect(() => {
    const nextPrime = primeGen.current.next().value;

    if (typeof nextPrime === "number") {
      const timer = setTimeout(
        () => setPrimes((prevPrimes) => [...primes, nextPrime]),
        1000 * primes.length
      );

      return () => clearTimeout(timer);
    }
  }, [primes]);

  return (
    <div className="App">
      <div>
        <ul>
          {primes.map((prime) => (
            <li key={prime}>{prime}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

I accepted @pilchard's answer, but I changed his example to more closely match what I was trying to do. I did make modifications to my original code, also, and now it does what I want.

Here's the summary of the changes, with comments.

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

/* 
I modified @pilchard's code to mimic the calculation-intensive generator
I have in my app. THIS IS NOT GOOD CODE, but I WANT this method to consume
cycles, rather than use setTimeout()
*/
function* genPrime(n) {
  if (isNaN(n) || !isFinite(n) || n % 1 || n < 2)
    return " Number not valid : " + n;

  for (var i = 2; i < n; i++) {
    if (isPrime(i)) {
      const then = Date.now();
      let end = false;

      // the following loop is intentionally labor-intensive
      do {
        const now = Date.now();
        if ( now - then > 1000) {
          end = true;
           yield i;
        }
      }  while (!end)
    }
  }
}

// no change here
function isPrime(num) {
  for (var i = 2; i < num; i++) {
    if (num % i === 0) {
      return false;
    }
  }
  return true;
}

export default function App() {
  const [primes, setPrimes] = useState([]);

  const primeGen = useRef(genPrime(100));

  useEffect(() => {
    const nextPrime = primeGen.current.next().value;

    // no setTimeout call here; delay is built into the generator
    if (typeof nextPrime === "number") {
      // const timer = setTimeout(
        // () => 
        setPrimes((prevPrimes) => [...primes, nextPrime]) //,
        // 1000 * primes.length
      // );

    //   return () => clearTimeout(timer);
    }
  }, [primes]);

  return (
    <div className="App">
      <div>
        <ul>
          {primes.map((prime) => (
            <li key={prime}>{prime}</li>
          ))}
        </ul>
      </div>
    </div>
  );
}

And as expected, this shows primes at one-second intervals.

Related