[] React hooks closure issue

demand analysis

We implemented a function like this

  • Click Start to begin execution interval, and once it is possible to go out into the lapse plus one
  • Click Stop to cancel interval
  • Click Clear cancels the interval, and set lapse 0
import React from 'react'
import ReactDOM from 'react-dom'

const buttonStyles = {
  border: '1px solid #ccc',
  background: '#fff',
  fontSize: '2em',
  padding: 15,
  margin: 5,
  width: 200,
}
const labelStyles = {
  fontSize: '5em',
  display: 'block',
}

function Stopwatch() {
  const [lapse, setLapse] = React.useState(0)
  const [running, setRunning] = React.useState(false)

  React.useEffect(() => {
    if (running) {
      const startTime = Date.now() - lapse
      const intervalId = setInterval(() => {
        setLapse(Date.now() - startTime)
      }, 0)
      return () => {
        clearInterval(intervalId)
      }
    }
  }, [running])

  function handleRunClick() {
    setRunning(r => !r)
  }

  function handleClearClick() {
    setRunning(false)
    setLapse(0)
  }

  if (!running) console.log('running is false')

  return (
    <div>
      <label style={labelStyles}>{lapse}ms</label>
      <button onClick={handleRunClick} style={buttonStyles}>
        {running ? 'Stop' : 'Start'}
      </button>
      <button onClick={handleClearClick} style={buttonStyles}>
        Clear
      </button>
    </div>
  )
}

function App() {
  const [show, setShow] = React.useState(true)
  return (
    <div style={{textAlign: 'center'}}>
      <label>
        <input
          checked={show}
          type="checkbox"
          onChange={e => setShow(e.target.checked)}
        />{' '}
        Show stopwatch
      </label>
      {show ? <Stopwatch /> : null}
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

Click into the demo test

 

Problem Description

1. We first click start, 2 and then click on the clear, 3 found the problem: the display is not 0ms

 

problem analysis

Why set a value of 0 through clear, but the display is not zero?

The main reason this happens is: useEffect is asynchronous, which means we are bound to perform the function useEffect or unbundling of functions, it is not updated in a setState generated synchronized execution. What does it mean? We might as well simulate the execution order code:
1. After we click to clear, we call setLapse and setRunning, these two methods are used to update the state, so they will mark the updated components, then we need to re-render notify React Come.
2. Then React to re-start the rendering process, and to quickly execute Stopwatch components.
3. The first implementation of the synchronization component Stopwatch components, and then perform asynchronous components, so by setting clear 0 is rendered, then asynchronous event useEffect is about to be executed since before executing the purge interval, interval there, it was calculated the latest value, and set to 0 by clear and rendered to change, then cleared.

Probably this order:
useEffect: the setRunning (to false) => setLapse (0) => the render (render) => performed Interval => (clearInterval => performed effect) => render (render)

 

problem solved

Method 1: Use useLayoutEffect

useLayoutEffect can be seen as the synchronous version useEffect. Can be achieved using useLayoutEffect we said above, the purpose of unbundling interval in the same update process.
useLayoutEffect inside the callback function will be executed immediately after the DOM update is complete, but will be run to completion before any paint, blocking the browser rendering in the browser.

Probably this order:
useLayoutEffect: the setRunning (to false) => setLapse (0) => the render (render) => (clearInterval => performed effect)

 

Method 2: Use useReducer solve the problem of closure

The lapse and running together, become a state subject, somewhat similar Redux usage. Here we give the TICK action plus a judgment whether running, in order to avoid unnecessary lapse changed after running is set to false.

So what difference does it achieve this with the use of updateLapse our way?

The biggest difference is our state does not come from the closure, in the previous code, we get lapse and running are closures by any method, but here, state is passed as a parameter to the Reducer, also that is, whenever we call dispatch, resulting in the Reducer is the latest in the State, which helped us avoid the problem of closure.

import React from 'react'
import ReactDOM from 'react-dom'

const buttonStyles = {
  border: '1px solid #ccc',
  background: '#fff',
  fontSize: '2em',
  padding: 15,
  margin: 5,
  width: 200,
}
const labelStyles = {
  fontSize: '5em',
  display: 'block',
}

const TICK = 'TICK'
const CLEAR = 'CLEAR'
const TOGGLE = 'TOGGLE'

function stateReducer(state, action) {
  switch (action.type) {
    case TOGGLE:
      return {...state, running: !state.running}
    case TICK:
      if (state.running) {
        return {...state, lapse: action.lapse}
      }
      return state
    case CLEAR:
      return {running: false, lapse: 0}
    default:
      return state
  }
}

function Stopwatch() {
  // const [lapse, setLapse] = React.useState(0)
  // const [running, setRunning] = React.useState(false)

  const [state, dispatch] = React.useReducer(stateReducer, {
    lapse: 0,
    running: false,
  })

  React.useEffect(
    () => {
      if (state.running) {
        const startTime = Date.now() - state.lapse
        const intervalId = setInterval(() => {
          dispatch({
            type: TICK,
            lapse: Date.now() - startTime,
          })
        }, 0)
        return () => clearInterval(intervalId)
      }
    },
    [state.running],
  )

  function handleRunClick() {
    dispatch({
      type: TOGGLE,
    })
  }

  function handleClearClick() {
    // setRunning(false)
    // setLapse(0)
    dispatch({
      type: CLEAR,
    })
  }

  return (
    <div>
      <label style={labelStyles}>{state.lapse}ms</label>
      <button onClick={handleRunClick} style={buttonStyles}>
        {state.running ? 'Stop' : 'Start'}
      </button>
      <button onClick={handleClearClick} style={buttonStyles}>
        Clear
      </button>
    </div>
  )
}

function App() {
  const [show, setShow] = React.useState(true)
  return (
    <div style={{textAlign: 'center'}}>
      <label>
        <input
          checked={show}
          type="checkbox"
          onChange={e => setShow(e.target.checked)}
        />{' '}
        Show stopwatch
      </label>
      {show ? <Stopwatch /> : null}
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

Guess you like

Origin www.cnblogs.com/fe-linjin/p/11412419.html
Recommended