2

I'm trying to create a hook, but I'm not sure where I've gone wrong conceptually, as it is unfortunately not working. I've written up a test example to recreate my problems - I'm unable to get isCalculating updates from my Page. Many thanks in advance for your help.

useTest.ts

import { useEffect, useState } from 'react'

const TEST_LENGTH = 100 * 10000000
const TEST_CRITERIA = 99 * 10000000

const useTest = () => {
  const [isCalculating, setIsCalculating] = useState(false)
  const [result, setResult] = useState(0)
  useEffect(() => {
    setIsCalculating(false)
  }, [result])
  const calculate = () => {
    setIsCalculating(true)
    for (let i = 0; i < TEST_LENGTH; i++) {
      if (i >= TEST_CRITERIA) {
        setResult(i)
        break
      }
    }
  }
  const reset = () => {
    setResult(0)
  }
  return { isCalculating, result, calculate, reset }
}

export default useTest

Test.tsx

import { Box, Button, Container, Text } from '@chakra-ui/react'
import { useTest } from '../helpers'

export default function Test() {
  const { isCalculating, result, calculate, reset } = useTest()
  return (
    <Container>
      <Box>
        <Text>Result: {result}</Text>
        {isCalculating ? <Text>is calculating...</Text> : null}
        <Button
          onClick={() => {
            result === 0 ? calculate() : reset()
          }}>
          <Text>{result === 0 ? 'Calculate' : 'Reset'}</Text>
        </Button>
      </Box>
    </Container>
  )
}

What I'm seeing is isCalculating updating in the hook only after the for loop calculation has finished, which for me is unexpected behavior. Any fixes to this?

What I've tried Tried to mimic setState callback with useEffect in useTest

import { useEffect, useState } from 'react'

const TEST_LENGTH = 100 * 10000000
const TEST_CRITERIA = 99 * 10000000

const useTest = () => {
  const [isCalculating, setIsCalculating] = useState(false)
  const [result, setResult] = useState(0)

  useEffect(() => {
    if (isCalculating) {
      for (let i = 0; i < TEST_LENGTH; i++) {
        if (i >= TEST_CRITERIA) {
          setResult(i)
          break
        }
      }
    }
  }, [isCalculating])
  const calculate = () => {
    setIsCalculating(true)
  }
  useEffect(() => {
    setIsCalculating(false)
  }, [result])
  const reset = () => {
    setResult(0)
  }
  return { isCalculating, result, calculate, reset }
}

export default useTest

Unfortunately (kind of expected) it still doesn't work.

CodeSandbox link: here As you can see, the I've set up an alert and some text to show on screen when isCalculating is true, unfortunately they both don't show until the calculation has completed.

Harrison
  • 653
  • 3
  • 6
  • 17
  • 1
    can you share your code with code sandbox? – egemenakturk Jun 13 '23 at 12:21
  • @egemenakturk sure here's the link: https://codesandbox.io/s/cranky-frost-x49p46?file=/package.json – Harrison Jun 13 '23 at 12:26
  • The loops are blocking the thread, you can wrap them in something like `setTimeout(() => {..}, 1)` [example](https://codesandbox.io/s/red-river-y87y4s?file=/src/useTest.js:431-563) – Titus Jun 13 '23 at 12:53
  • @Titus Oh, that's amazing. It totally works. Wondering if this is the only way though...seems kind of like a hack...Is this how other people do it? – Harrison Jun 13 '23 at 12:56
  • the `useEffect` version (2nd version of hook) actually works if you `console.log(isCalculating)` during render, just seems somehow painting part is still blocked by the loop. With first version of the `useTest` I guess I'd expect it to work the way it does. – Giorgi Moniava Jun 13 '23 at 12:58
  • I'm not sure how common that method is. You can see [here](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#late_timeouts) a description of why it works. – Titus Jun 13 '23 at 13:02

1 Answers1

1

React automatically batches all the setState calls to one, in the useEffect to reduce the re-renders.

In your case, it batches all the setState calls at the end (waiting for for loop to end - blocking)

You can also have a look at startTransition() method which will do the same work in the different scenario.

Discussion: https://github.com/reactwg/react-18/discussions/21

Similar: React batch updates for multiple setState() calls inside useEffect hook

Shri Hari L
  • 4,551
  • 2
  • 6
  • 18