I am writing a React hook that allows me to use setInterval
in my ponents. In order to do so I need to keep the latest version of the callback in a ref so that it can be accessed from the scope of the interval later on.
This is my code so far:
import { useRef, useEffect } from 'react'
export default function useInterval(
callback: () => void,
delay: number | null,
) {
const callbackRef = useRef(callback)
// Remember the latest callback.
useEffect(() => {
callbackRef.current = callback
})
useEffect(() => {
// Don't schedule if no delay is specified.
if (delay === null) {
return
}
const id = setInterval(() => callbackRef.current(), delay)
return () => clearInterval(id)
}, [delay])
}
My question is about the first instance of useEffect
where the latest value is passed to the ref. According the the React documentation this code will execute after my ponent has rendered.
I can imagine this is useful when you are passing a ref to an element so you can be sure that it has a value after it has rendered. But if my code doesn't care about when the ponent renders, does it still make sense to keep this in a useEffect
?
Would it make sense that I rewrite the code as follows:
import { useRef, useEffect } from 'react'
export default function useInterval(
callback: () => void,
delay: number | null,
) {
const callbackRef = useRef(callback)
// Remember the latest callback.
callbackRef.current = callback
useEffect(() => {
// Don't schedule if no delay is specified.
if (delay === null) {
return
}
const id = setInterval(() => callbackRef.current(), delay)
return () => clearInterval(id)
}, [delay])
}
I am writing a React hook that allows me to use setInterval
in my ponents. In order to do so I need to keep the latest version of the callback in a ref so that it can be accessed from the scope of the interval later on.
This is my code so far:
import { useRef, useEffect } from 'react'
export default function useInterval(
callback: () => void,
delay: number | null,
) {
const callbackRef = useRef(callback)
// Remember the latest callback.
useEffect(() => {
callbackRef.current = callback
})
useEffect(() => {
// Don't schedule if no delay is specified.
if (delay === null) {
return
}
const id = setInterval(() => callbackRef.current(), delay)
return () => clearInterval(id)
}, [delay])
}
My question is about the first instance of useEffect
where the latest value is passed to the ref. According the the React documentation this code will execute after my ponent has rendered.
I can imagine this is useful when you are passing a ref to an element so you can be sure that it has a value after it has rendered. But if my code doesn't care about when the ponent renders, does it still make sense to keep this in a useEffect
?
Would it make sense that I rewrite the code as follows:
import { useRef, useEffect } from 'react'
export default function useInterval(
callback: () => void,
delay: number | null,
) {
const callbackRef = useRef(callback)
// Remember the latest callback.
callbackRef.current = callback
useEffect(() => {
// Don't schedule if no delay is specified.
if (delay === null) {
return
}
const id = setInterval(() => callbackRef.current(), delay)
return () => clearInterval(id)
}, [delay])
}
Share
Improve this question
asked Mar 25, 2021 at 16:20
Jon KoopsJon Koops
9,3116 gold badges32 silver badges54 bronze badges
1
- If you omit the second parameter in useEffect, it will execute that code on the first render and on every re-render, in which case, useEffect has no effect and you might as well just put it in the body of the function. If you have eslint enabled, it should actually plain to you when you do that. – Mike Commented Mar 25, 2021 at 16:40
1 Answer
Reset to default 8When to use useEffect
without dependencies vs. direct assignment?
From docs:
The Effect Hook lets you perform side effects in function ponents
- A
useEffect
without dependencies (orundefined
as dependencies) will be run at first render and every subsequent re-render, But always as a side-effect i.e. after the ponent has rendered. - A direct assignment (a sync operation) will be run at first render and every subsequent re-render, But always as in the render cycle. It may affect performance or delay the rendering.
So, when to use which one? It depends on your use case.
Which one to use in this (in question) use case?
I would say neither
useEffect(() => {
callbackRef.current = callback
})
nor
callbackRef.current = callback
seems correct in this use case.
Because we don't want to do the assignment - callbackRef.current = callback
at every re-render. But we want to do it when there is a change in callback
. So, the below one seems better:
useEffect(() => {
callbackRef.current = callback
}, [callback])
You may see this blog and this related post.
A demo which shows that an effect runs after as a side-effect (Log inside effect is always the last):
function useInterval(callback, delay) {
const callbackRef = React.useRef(callback)
React.useEffect(() => {
callbackRef.current = callback
}, [callback])
React.useEffect(() => {
if (delay !== null) {
const id = setInterval(() => callbackRef.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
function Demo() {
const [count, setCount] = React.useState(0)
function doThis() {
setCount(count + 1)
}
useInterval(doThis, 1000)
console.log('log - before effect')
React.useEffect(() => {
console.log('log inside effect')
})
console.log('log - after effect')
return <h1>{count}</h1>
}
ReactDOM.render(<Demo />, document.getElementById('root'))
<script crossorigin src="https://unpkg./react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg./react-dom@17/umd/react-dom.production.min.js"></script>
<body>
<div id="root"></div>
</body>