2

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.

Jeff Lowery
  • 2,492
  • 2
  • 32
  • 40
  • Does this answer your question? [Update React component every second](https://stackoverflow.com/questions/39426083/update-react-component-every-second) – pilchard Dec 29 '20 at 00:16
  • No. If you look at testPrime, you see it calls setPrime at 0, 1, 2 second intervals, and the list is incrementally updated. The generator takes awhile to generate each prime, and then I call setPrime in a setTimeout(). This was suggested as a way for the renderer to get time in the thread (I'm not sure that is correct, as rendering occurs in a different thread). – Jeff Lowery Dec 29 '20 at 00:23
  • It may be I can't do what I want in a JSX component. – Jeff Lowery Dec 29 '20 at 00:25
  • Regardless of whether you are changing the timing each call, the answer I linked deals with the basics of timing (and cleanup, which you aren't doing in your useEffect). see the docs: [Timing of effects](https://reactjs.org/docs/hooks-reference.html#timing-of-effects) – pilchard Dec 29 '20 at 00:28
  • The answer has nothing to do with incrementally updating a list. – Jeff Lowery Dec 29 '20 at 00:30
  • I don't see why you are storing components in state, just map the list in the return, how to map a list is a different question than the timing issues that you're having. – pilchard Dec 29 '20 at 00:31
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/226534/discussion-between-jeff-lowery-and-pilchard). – Jeff Lowery Dec 29 '20 at 00:32
  • are you just wanting the visual effects? – Someone Special Dec 29 '20 at 00:59
  • I have a theory as to what the problem is. When the generator yields, it commences generating again. Since it is a calculation-intensive action, React has no time to render the previous result. It's getting that slice of time to render that is the problem. The testPrime() function uses setTimeout to increment `primes`. The generator does not: it yields a result and then continues looking for the next. That's my theory. I'll test that. – Jeff Lowery Dec 29 '20 at 01:09
  • So I tried generating more primes, and it looks like the parent component is completely re-rendering itself as primes are being generated. Perhaps I have to use an component that's based on React.Component, so that I can use componentShouldUpdate()? Pure JSX components have limitations is seems. – Jeff Lowery Dec 29 '20 at 01:16

2 Answers2

2

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>
  );
}
pilchard
  • 12,414
  • 5
  • 11
  • 23
  • I don't know if useRef will make any difference. Could you explain to me why it would? – Jeff Lowery Dec 29 '20 at 01:39
  • `useRef` maintains its value through renders without triggering them. I just posted a sandbox with a working generator example (it wouldn't run in a snippet). – pilchard Dec 29 '20 at 01:40
  • okay, thanks. I will take a look tomorrow. – Jeff Lowery Dec 29 '20 at 01:47
  • Thanks @pilchard, I marked this as the answer. The `useRef` call was key. Here's an blog post that gives an excellent description of what `useRef` does: https://www.smashingmagazine.com/2020/11/react-useref-hook/ I changed your sandbox code a bit to more closely match what was happening in my code, which I will show in the original post. – Jeff Lowery Dec 29 '20 at 20:08
  • Well, not quite. By using useRef, new arguments to genPrime() do not change the behavior of the ref object. But I think I can work that out. – Jeff Lowery Jan 13 '21 at 00:50
0

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.

Jeff Lowery
  • 2,492
  • 2
  • 32
  • 40