Author: ICBU Dongmo
Write at the front: welcome to Alibaba ICBU interactive & end technology front end team column, we will share quality front end technology articles with you, welcome to pay attention to & exchange yo!
This article assumes that you have the following knowledge or experience:
- The React > = 16.9
- React Class Component
- React Functional Component
- The React Hooks, mainly useState/useEffect/useRef
My name is dongmo. If you need new job opportunities, please contact me at 👉 dongmo.cl#alibaba-inc.com :js.chenlei.me
.useRef vs .useState
In this article, we refer to. UseRef, which is a react-hooks device. Press ↑↓↓ ↓←→←→BA to add 30 lives: It holds values in multiple renders of the React Functional Component in exactly the order in which you set/get, like Hooks APIS do. This is different from the [state, updater] returned by.usestate
const ref = useRef(initVal)
const [state, updater] = useState(initVal)
get
ref.current
state
set
ref.current = newVal
updater(newVal)
Whether the set causes React Fiber scheduling
❌
✅
Is set/get intuitive
✅
❌
Use.useref when you can get the latest state value in a functional component as you always intuitively expect.
Dependency List (Deps)
In the case of Hooks, their dependencies are a feature that developers must take into account, which, if ignored or understood incorrectly, can have devastating side effects on components and applications — for example, the effect of infinite loops.
The following component, once referenced to the component, will send getJSON requests repeatedly, crashing the application.
function InfiniteRequestHookComponent () { React.useEffect(() => { getJSON(...) })}Copy the code
We need to give useEffect a dependency list — at least an empty array.
function SafeHookComponent () {
React.useEffect(() => {
getJSON(...)
}, []) // add one deps list
}
Copy the code
Understanding the DEPS is as important as understanding these rules in the React Class Component:
- State can only be initialized in constructor
- Props are immutable objects
- The componentDidMount pair will only be called once in a component’s Lifecycle
To write a stable and reliable component using React Hook, you must understand the Hooks dependency list (hereafter referred to collectively as DEPS) and then handle those scenarios
When is dePS []?
You may have seen the introduction elsewhere: When converting a React Class Component to a React Function Component, put the data request logic in componentDidMount in React. UseEffect (callback, []) callback, like this:
// class component class FooClassComponent extends React.Component { componentDidMount() { asyncRequest() .then((result) => { // deal with your result }) } } // function component function FooFunctionalComponent () { React.useEffect(() => { asyncRequest() .then((result) => { // deal with your result }) }, []) }Copy the code
If the react. useEffect deps list is an empty array, it means that the business logic in it is executed only once in the FooFunctionalComponent (when the component first renders), and after that, No matter how many times the FooFunctionalComponent re-render, the business logic will not be executed — since deps is empty, Effect will not be re-executed for any external reason.
This mechanism is similar to how componentDidMount behaves for the entire FooClassComponent lifecycle: ComponentDidMount is executed only after the first time the component completes rendering, no matter how many times the FooClassComponent re-renders.
ComponentDidMount is not equivalent to React. UseEffect (callback, []) because the scheduler is not the same, but the scheduler can perform similar functions. Keep this in mind: the implementation mechanism of Functional Component Hooks is different from the implementation mechanism of Class Component Lifecycle.
What happens if DEPS is not empty?
There are scenarios like this: when the user is typing in , we want to be able to do some asynchronous actions in real time with the user’s input, such as:
- The real-time calibration
- The remote search
- .
In the case of remote search, which can be described by Hooks:
function SearchComponent () {
const [ keyword, setKeyword ] = React.useState('');
// hook1
React.useEffect(() => {
// callback: do some search action against keyword
searchByKeyword(keyword)
.then(result => {
// process search result
})
}, [ keyword ])
return (
<input
value={keyword}
onChange={(evt) => {
setKeyword(evt.target.value || '')
}} />
)
}
Copy the code
Here is a hook1(.useeffect) whose deps is [keyword] — meaning that when the keyword changes, the.useeffect (callback, deps) callback will be executed again; When the user enters, input[onChange] is triggered, where setKeyword not only causes a keyword update, but also a re-rendering of the component.
Use the cheater.useref
As mentioned above, useRef provides a container to hold values and allows you to read them in strict order.
Such as
const sthRef = useRef(null)
sthRef.current = 1;
setTimout(() => {
sthRef.current = 3;
}, 3000);
setTimeout(() => {
sthRef.current = 9;
}, 9000);
Copy the code
Sthref. current starts with null and is immediately updated to 1, 3 after 3s, and 9 after 9s.
Unlike.usestate, updating sthref. current does not cause Functional Comopnent’s re-render.
Implement a useTimeout step by step
At the end of this article, we left a question about how to provide an appropriate useTimeout to overcome the closure problem so that after 3s, count in useTimeout is the latest value 5(because it was updated in useEffect).
const TimeoutExample = () => { const [count, setCount] = React.useState(0) const [countInTimeout, setCountInTimeout] = React.useState(0) React.useEffect(() => { setTimeout(() => { // count at next line equals to `0` :( due to closure issue. // can we provide one useful `useTimeout` update whole callback of `setTimeout`? setCountInTimeout(count) }, 3000) setCount(5) }, []) return ( <div> Count: {count} <br /> setTimeout Count: {countInTimeout} </div> ) }Copy the code
In this article, we used countRef to save the value of count, which fixes the Hooks closure trap, but this is not universal. Next time we need to save a similar value, we need to create xxxRef. If the closure of callback causes us to fail to get the latest value of count, which is fixed by Hooks in combination with setTimeout(callback, 3000), can we try updating the closure? Based on this idea, we propose useTimeout, hoping to directly update the entire setTimeout(callback, 3000) callback. If this can be implemented, then the final writing method is similar to the following:
const TimeoutExample = () => {
const [count, setCount] = React.useState(0)
const [countInTimeout, setCountInTimeout] = React.useState(0)
useTimeout(() => {
setCountInTimeout(count)
}, 3000, [ count ])
useEffect(() => {
setCount(5)
}, [])
return (
<div>
Count: {count}
<br />
setTimeout Count: {countInTimeout}
</div>
)
}
Copy the code
Copy
Instead of deps, we just want to save the two parameters cb and timeout first, and we want to call setTimeout when appropriate to start the timer. Starting the timer is a side effect. We put it in.useeffect () :
function useTimeout (cb, timeout) {
const [callback, setCallback] = React.useState(cb)
React.useEffect(() => {
setTimeout(callback, timeout)
}, [])
}
Copy the code
However, if we use.usestate to store cb, the reference.useTimeout() component will also be updated each time setCallback is performed — for our purposes “updating the entire closure when count changes “, Obviously we need to update callback, but the resulting view update doesn’t seem to be necessary, so we’ll use the cheat and save cb instead with.useref:
function useTimeout (cb, timeout) {
const callbackRef = React.useRef(cb)
React.useEffect(() => {
setTimeout(callback, timeout)
}, [])
}
Copy the code
Copy
Now that we haven’t done the “update callback” thing, let’s review our goal: when the count changes, our saved callback should change as well. So we put count in the deps of.useeffect and update callbackref.current in.useeffect:
function useTimeout (cb, timeout, count) {
const callbackRef = React.useRef(cb)
React.useEffect(() => {
// update it if count updated
callbackRef.current = cb;
setTimeout(callback, timeout)
// count as item of deps
}, [count])
}
Copy the code
However, passing count is only suitable for this scenario. In other scenarios, others may wish to pass other parameters, so it is better to pass the third parameter as deps, and the user can pass whatever they like:
// user should put `count` in deps
function useTimeout (cb, timeout, deps = []) {
const callbackRef = React.useRef(cb)
React.useEffect(() => {
// update it if count updated
callbackRef.current = cb;
setTimeout(callback, timeout)
}, deps)
}
// user should put `count` in deps
useTimeout(cb, 3000, [ count ])
Copy the code
The.useEffect(effect, deps) effect is executed again every time there is an update in the DEps, but this effect has a setTimeout. Const timerId = setTimeout(…) The timer will not be removed from the event queue until clearTimeout(timerId) is cancelled or executed. When the count changes and.useEffect(effect, deps) effect is executed again, Before we cancel the timer generated by the previous setTimeout, we create a new timer = setTimeout.
This is obviously not desirable: in this scenario, if we are updating the callbackref.current, the previously unexecuted timer should be cancelled (of course, the already executed timer is not included), so let’s do this manually:
function useTimeout (cb, timeout, deps = []) {
const callbackRef = React.useRef(cb)
const timerRef = React.useRef(null)
React.useEffect(() => {
callbackRef.current = cb;
if (timerRef.current) {
clearTimeout(timerRef.current)
}
timerRef.current = setTimeout(callback, timeout)
}, deps)
}
Copy the code
As shown above, we used the cheater again, so why don’t we use a let timer = null to save the previous timer? I’m sure you’re smart enough to figure it out.
However, we duck do not have to save the previous timer. React.useEffect(effect, deps) to allow an effect to return its dispose function, if the developer does return it, When Functional Componnet is next run (re-render) to React. UseEffect (effect, DEPS), it calls its dispose function, like this:
React.useEffect(() => {
// some side effect here
return () => { // dispose function
// clear some side effect here
}
}, deps)
Copy the code
So for useTimeout, if we want to eliminate the timer generated by setTimeout in the previous effect every time we update CB, we can also write:
function useTimeout (cb, timeout, deps = []) {
const callbackRef = React.useRef(cb)
React.useEffect(() => {
callbackRef.current = cb;
const timerId = setTimeout(cb, timeout)
return () => {
clearTimeout(timerId)
}
}, deps)
}
Copy the code
Instead, we made use of the closure feature of Dispose to eliminate the side effects of the previous effect simply and accurately.
Is that it? This is logically sorted out, but we can add a little detail to make useTimeout more robust:
function useTimeout (cb, timeout, deps = []) { const callbackRef = React.useRef(cb) React.useEffect(() => { if (timeout < 0 || typeof callbackRef.current ! == 'function') return; callbackRef.current = cb; const timerId = setTimeout(cb, timeout) return () => { clearTimeout(timerId) } }, deps) }Copy the code
Let’s see if useTimeout meets the requirements we outlined at the end of this article, check out the Live Demo, and wait for 3s after startup to see if the view is updated as expected 🙂
❤️ Thank you for seeing the end ~
Alibaba international website (ICBU, Alibaba.com) is the world’s largest cross-border trade and service platform. We have new technical challenges all the time, enough interesting challenges to satisfy all your curiosity and thirst for knowledge, and well-known foreign partners (Google & OpenSky).
If you want to come to ICBU to develop the front end with me, please send your resume to [email protected] and we will respond to your interview arrangement quickly. : -)