React supports Hooks since version 16.8.0. This allows us to use state and other React features in function components. This is a great convenience, but it also adds some learning costs.

For example, if we need a countdown of 10s, the following code has some problems:

// Error example
import React, { useEffect, useState } from 'react'
const Timer = () = > {
  const [count, setCount] = useState(10)
  const otherFn = () = > {
    console.log('otherFn')
  }
  useEffect(() = > {
    const timer = setInterval(() = > {
      if (count === 0) {
        clearInterval(timer)
        return
      }
      console.log('count', count)
      setCount(count - 1)},1000)
    otherFn() // a method that is called only once
    return () = > {
      console.log('clean')
      clearInterval(timer)
    }
  }, []) // Avoid otherFn running multiple times, passing an empty array
  return <div>The remaining {count} seconds</div>
}

export default Timer
Copy the code

When I run it, I can see that the count changes from 10 to 9 and it doesn’t decrease by one. From the count printed on the console, I can see that the timer is always running, except that count is always 10.

This is because we pass an empty array to the second argument to useEffect, so the hook is only run once when the component is mounted and not run again depending on the change in count. The function component generates a separate version (a closure) each time it renders, and each version has its own count. The Timer component’s count was 10 in its first version, and it always gets 10 no matter how long the delay was.

So how do we fulfill our requirements?

Writing count to useEffect(() => {}, [count]) seems to work, and it does, but it causes the timer to reset so frequently that setInterval is like setTimeout.

And the method otherFn, which we only need to call once, will also be called frequently. So this method is not desirable.

React offers three solutions:

  • Update count functionally

setCount(c => c - 1)

This.setstate (() => {}) gets the latest count inside the function, but the count inside the timer outside the function is still 10. If (count === 0) {clearInterval(timer) return} cannot be processed by determining the value of count. Therefore, this scheme passes.

  • 2, The useReducer Hook moves the state update logic out of effect

Adamrackis. Dev /state-and-u…

Writing a lot of template code is a bit of an overkill for simple logic like ours, so this approach is not recommended, so I won’t go over it here.

  • 3. Use ref to save variables

UseRef returns a mutable ref object. We store the latest value of count in the ref current property.

import React, { useEffect, useState, useRef } from 'react'

const Timer = () = > {
  const [count, setCount] = useState(10)
  const latestCount = useRef(count) // define a ref with an initial value of 10
  const otherFn = () = > {
    console.log('otherFn')
  }
  useEffect(() = > {
    latestCount.current = count / / update
  })
  useEffect(() = > {
    const timer = setInterval(() = > {
      if (latestCount.current === 0) { // Latestcount.current, not count
        clearInterval(timer)
        return
      }
      setCount(c= > c - 1)},1000)
    otherFn()
    return () = > {
      clearInterval(timer)
    }
  }, [])
  return <div>{count}</div>
}

export default Timer
Copy the code

Here we use two useeffects. In the first useEffect we point latestCount.current to count, similar to this.count in the class component, and in the other useEffect we handle other logic. This will fulfill our requirements.

Then it’s time to think

  • 1) We know that hook is actually a JS function, so can we extract the first useEffect and customize a hook?
import { useRef } from 'react'

const useValueRef = (params: any) = > {
  const paramsRef = useRef(null)
  paramsRef.current = params
  return paramsRef
}

export default useValueRef
Copy the code

So our code can be further rewritten with useValueRef to remove the first useEffect:

import React, { useEffect, useState } from 'react'
import useValuesRef from './useValuesRef.ts'

const Timer = () = > {
  const [count, setCount] = useState(10)
  const latestCount = useValuesRef(count) // useValuesRef
  const otherFn = () = > {
    console.log('otherFn')
  }
  useEffect(() = > {
    const timer = setInterval(() = > {
      if (latestCount.current === 0) {
        clearInterval(timer)
        return
      }
      setCount(c= > c - 1)},1000)
    otherFn()
    return () = > { 
      clearInterval(timer)
    }
  }, [])
  return <div>{count}</div>
}

export default Timer
Copy the code

This makes our code much clearer and easier to read.

  • 2) Let’s go back to the two useEffect codes. Can we define timer in one useEffect and process logic in the other useEffect?
// Error example
const [count, setCount] = useState(10)
let timer
useEffect(() = > {
    otherFn()
    timer = setInterval(() = > {
      setCount(c= > c - 1)},1000)
    return () = > {
      clearInterval(timer)
    }
  }, [])

  useEffect(() = > {
    if (count === 0) {
      clearInterval(timer) // Can the timer be cleared here?
      return
    }
  }, [count])
Copy the code

The above code makes the same mistake as the initial code, the function component generates a new timer every time it renders, so the second useEffect does not clear the timer set on the first rendering. The solution is also to use ref:

const [count, setCount] = useState(10)
const timer = useRef(null)
useEffect(() = > {
    otherFn()
    timer.current = setInterval(() = > {
      setCount(c= > c - 1)},1000)
    return () = > {
      clearInterval(timer.current)
    }
  }, [])

  useEffect(() = > {
    if (count === 0) {
      clearInterval(timer.current) // The timer can be cleared successfully
      return
    }
  }, [count])
Copy the code
  • 3) We can also try to remove the second useEffect from 2) so that there is only one useEffect in the component, which requires a new way of thinking:
import { useEffect } from 'react'
import useValuesRef from './useValuesRef.ts'

const useInterval = (callback, delay) = > {
  const savedCallback = useValuesRef(callback)

  useEffect(() = > {
    if(delay ! = =null) {
      const timer = setInterval(() = > {
        savedCallback.current()
      }, delay)
      return () = > {
        clearInterval(timer) // When the delay changes, the old timer is cleared
      }
    }
  }, [delay])
}

export default useInterval
Copy the code

Instead of saving timer to ref, we change interval’s input parameter to variable, so that the count value is updated each time, and when delay changes, our timer will be cleared, and if we pass delay null, the timer will not be re-created. Rewrite the code as follows:

import React, { useEffect, useState } from 'react'
import useInterval from './useInterval.ts'

const Timer = () = > {
  const [count, setCount] = useState(10)
  const otherFn = () = > {
    console.log('otherFn')
  }

  useInterval(() = > {
    setCount(count - 1) // Every render goes here, so count is up to date
  }, count === 0 ? null : 1000)

  useEffect(() = > {
    otherFn()
  }, [])

  return <div>{count}</div>
}

export default Timer
Copy the code

In this way, we can also implement more functions, such as adding a button to pause and reset the timer, and so on. Kids can try it out for themselves. This way can be said once and for all, behind other components can be directly used, convenient and fast.

conclusion

This paper mainly introduces three ideas to avoid the timer problem caused by the closure of function components. In this paper, the method of using REF is implemented in detail, three kinds of optimization are carried out at the same time, two customized hooks are defined, and finally method 3 is recommended. Of course, other methods have their own advantages, and can be selected by themselves in actual use. Boys, are you out of school?

Refer to the article: react.docschina.org/docs/hooks-… Overreacted. IO/useful – Hans/a – c… Overreacted. IO/making – seti…