Skip to main content
update with `useMemo`
Source Link
skyboyer
  • 24k
  • 7
  • 65
  • 73

After some time passed I'm sure it's much easier to handle things by your own with setTimeout/clearTimeoutFrom the 2025 the best pattern I saw yet is(and movingother users noticed that into separate custom hookbelow, please upvote their answers not mine) than working with functional helpers. Handling later one creates additional challenges right after:

 const [search, setSearch] = useState('') const onSearch = useMemo( () => debounce((phrase: string) => { setSearch(phrase) }, SEARCH_DELAY), [], ) useEffect(() => { return () => onSearch.cancel() }, [onSearch]) 

For controlled component, though, we apply thatdon't want to have a delay between keypress and character appearing in the field. In this case you may have useCallback that can be recreated because of dependency changetwo different state - one for the field value which is updated immediately and another - debounced - for a search value.

There were other users were noticing this approach, but we don'tmy answer is listed first and I want to reset delay runningcommunicate that old answer is not relevant any more(or have never been?).

original answer below

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

After some time passed I'm sure it's much easier to handle things by your own with setTimeout/clearTimeout(and moving that into separate custom hook) than working with functional helpers. Handling later one creates additional challenges right after we apply that to useCallback that can be recreated because of dependency change but we don't want to reset delay running.

original answer below

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

From the 2025 the best pattern I saw yet is(and other users noticed that below, please upvote their answers not mine):

 const [search, setSearch] = useState('') const onSearch = useMemo( () => debounce((phrase: string) => { setSearch(phrase) }, SEARCH_DELAY), [], ) useEffect(() => { return () => onSearch.cancel() }, [onSearch]) 

For controlled component, though, we don't want to have a delay between keypress and character appearing in the field. In this case you may have two different state - one for the field value which is updated immediately and another - debounced - for a search value.

There were other users were noticing this approach, but my answer is listed first and I want to communicate that old answer is not relevant any more(or have never been?).

original answer below

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

added 407 characters in body
Source Link
skyboyer
  • 24k
  • 7
  • 65
  • 73

After some time passed I'm sure it's much easier to handle things by your own with setTimeout/clearTimeout(and moving that into separate custom hook) than working with functional helpers. Handling later one creates additional challenges right after we apply that to useCallback that can be recreated because of dependency change but we don't want to reset delay running.

original answer below

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

After some time passed I'm sure it's much easier to handle things by your own with setTimeout/clearTimeout(and moving that into separate custom hook) than working with functional helpers. Handling later one creates additional challenges right after we apply that to useCallback that can be recreated because of dependency change but we don't want to reset delay running.

original answer below

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

added 36 characters in body
Source Link
skyboyer
  • 24k
  • 7
  • 65
  • 73

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback isin case of delayed run does not more suitable hereprovide significant advantage. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback is not more suitable here. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

you may(and probably need) useRef to store value between renders. Just like it's suggested for timers

Something like that

const App = () => { const [value, setValue] = useState(0) const throttled = useRef(throttle((newValue) => console.log(newValue), 1000)) useEffect(() => throttled.current(value), [value]) return ( <button onClick={() => setValue(value + 1)}>{value}</button> ) } 

As for useCallback:

It may work too as

const throttled = useCallback(throttle(newValue => console.log(newValue), 1000), []); 

But if we try to recreate callback once value is changed:

const throttled = useCallback(throttle(() => console.log(value), 1000), [value]); 

we may find it does not delay execution: once value is changed callback is immediately re-created and executed.

So I see useCallback in case of delayed run does not provide significant advantage. It's up to you.

[UPD] initially it was

 const throttled = useRef(throttle(() => console.log(value), 1000)) useEffect(throttled.current, [value]) 

but that way throttled.current has bound to initial value(of 0) by closure. So it was never changed even on next renders.

So be careful while pushing functions into useRef because of closure feature.

added 488 characters in body
Source Link
skyboyer
  • 24k
  • 7
  • 65
  • 73
Loading
added 24 characters in body
Source Link
skyboyer
  • 24k
  • 7
  • 65
  • 73
Loading
added 24 characters in body
Source Link
skyboyer
  • 24k
  • 7
  • 65
  • 73
Loading
Source Link
skyboyer
  • 24k
  • 7
  • 65
  • 73
Loading