I'm reading React Hook documentation about functional updates and see this quote:
The ”+” and ”-” buttons use the functional form, because the updated
value is based on the previous value
But I can't see for what purposes functional updates are required and what's the difference between them and directly using old state in computing new state.
Why functional update form is needed at all for updater functions of React useState Hook? What are examples where we can clearly see a difference (so using direct update will lead to bugs)?
For example, if I change this example from documentation
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
);
}
to updating count directly:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</>
);
}
I can't see any difference in behaviour and can't imagine case when count will not be updated (or will not be the most recent). Because whenever count is changing, new closure for onClick will be called, capturing the most recent count.
State update is asynchronous in React. So it is possible that there would be old value in count when you're updating it next time. Compare, for example, result of these two code samples:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1)}
}>+</button>
</>
);
}
and
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => {
setCount(count + 1);
setCount(count + 1)}
}>+</button>
</>
);
}
I stumbled into a need for this recently. For example let's say you have a component that fills up an array with some amount of elements and is able to append to that array depending on some user action (like in my case, I was loading a feed 10 items at a time as the user kept scrolling down the screen. the code looked kind of like this:
function Stream() {
const [feedItems, setFeedItems] = useState([]);
const { fetching, error, data, run } = useQuery(SOME_QUERY, vars);
useEffect(() => {
if (data) {
setFeedItems([...feedItems, ...data.items]);
}
}, [data]); // <---- this breaks the rules of hooks, missing feedItems
...
<button onClick={()=>run()}>get more</button>
...
Obviously, you can't just add feedItems to the dependency list in the useEffect hook because you're invoking setFeedItems in it, so you'd get in a loop.
functional update to the rescue:
useEffect(() => {
if (data) {
setFeedItems(prevItems => [...prevItems, ...data.items]);
}
}, [data]); // <--- all good now
I have answered a similar question like this and it was closed because this was the canonical question - that i did not know of, upon looking the answers i decided to repost my answer here since i think it adds some value.
If your update depends on a previous value found in the state, then you should use the functional form. If you don't use the functional form in this case then your code will break sometime.
Why does it break and when
React functional components are just closures, the state value that you have in the closure might be outdated - what does this mean is that the value inside the closure does not match the value that is in React state for that component, this could happen in the following cases:
1- async operations (In this example click slow add, and then click multiple times on the add button, you will later see that the state was reseted to what was inside the closure when the slow add button was clicked)
const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
}}
>
immediately add
</button>
<button
onClick={() => {
setTimeout(() => setCounter(counter + 1), 1000);
}}
>
Add
</button>
</>
);
};
2- When you call the update function multiple times in the same closure
const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
setCounter(counter + 1);
}}
>
Add twice
</button>
</>
);
}
The “state update is asynchronous in React” answer is misleading, as are some comments below it. My thinking was also wrong until I dug into this further. You are right, this is rarely needed.
The key idea behind functional state updates is that state you depend on for the new state might be stale. How does state get stale? Let’s dispel some myths about it:
Myth: State can be changed under you during event handling.
Fact: The ECMAScript event loop only runs one thing at a time. If you are running a handler, nothing else is running alongside it.
Myth: Clicking twice fast (or any other user action happening quickly) can cause state updates from both handler calls to be batched.
Fact: React is guaranteed to not batch updates across more than one user-initiated event. This is true even in React 18, which does more batching than previous versions. You can rely on having a render in between event handlers.
From the React Working Group:
Note: React only batches updates when it’s generally safe to do. For example, React ensures that for each user-initiated event like a click or a keypress, the DOM is fully updated before the next event. This ensures, for example, that a form that disables on submit can’t be submitted twice.
So when do you get stale state?
Here are the main 3 cases I can think of:
Multiple state updates in the same handler
This is the case already mentioned where you set the same state multiple times in the same handler, and depend on the previous state. As you pointed out, this case is pretty contrived, because this clearly looks wrong:
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}
>+</button>
A more plausible case is calling multiple functions that each do updates on the same state and depend on the previous state. But that’s still weird, it’d make more sense to do all the calculations then set the state once.
Async state updates in a handler
For example:
<button
onClick={() => {
doSomeApiCall().then(() => setCount(count + 1));
}}
>+</button>
This is not so obviously wrong. The state can be changed in between you calling doSomeApiCall and when it resolves. In this case, the state update really is async, but you made it that way, not React!
The functional form fixes this:
<button
onClick={() => {
doSomeApiCall().then(() => setCount((currCount) => currCount + 1));
}}
>+</button>
Updating state in useEffect
G Gallegos's answer pointed this out for useEffect in general, and letvar's answer pointed this out for useEffect with requestAnimationFrame. If you're updating state based on previous state in useEffect, putting that state in the dependency array (or not using a dependency array) is a recipe for infinite loops. Use the functional form instead.
Summary
You don’t need the functional form for state updates based on previous state, as long as you do it 1. in a user-triggered-event handler 2. once per handler per state and 3. synchronously. If you break any of those conditions, you need functional updates.
Some people might prefer to always use functional updates, so you don’t have to worry about those conditions. Others might prefer the shorter form for clarity when it’s safe to do so, which is true for many handlers. At that point it’s personal preference / code style.
Historical note
I learned React before Hooks, when only class components had state. In class components, “multiple state updates in the same handler” doesn’t look so obviously wrong:
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}}
>+</button>
Since state is an instance variable instead of a function parameter, this looks fine, unless you know that setState batches calls when in the same handler.
In fact, in React <= 17, this would work fine:
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}, 1000);
Since it’s not an event handler, React re-renders after each setState call.
React 18 introduces batching for this and similar cases. This is a useful performance improvement. There is the downside that it breaks class components that rely on the above behavior.
References
React Working Group discussion
ehab’s answer, which also mentions the two cases where functional updates are needed.
Another use case for using functional updates with setState - requestAnimationFrame with react hooks. Detailed information is available here - https://css-tricks.com/using-requestanimationframe-with-react-hooks/
In summary, handler for requestAnimationFrame gets called frequently resulting in incorrect count value, when you do setCount(count+delta). On the other hand, using setCount(prevCount => prevCount + delta) yields correct value.
Related
I'm reading React Hook documentation about functional updates and see this quote:
The ”+” and ”-” buttons use the functional form, because the updated
value is based on the previous value
But I can't see for what purposes functional updates are required and what's the difference between them and directly using old state in computing new state.
Why functional update form is needed at all for updater functions of React useState Hook? What are examples where we can clearly see a difference (so using direct update will lead to bugs)?
For example, if I change this example from documentation
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
</>
);
}
to updating count directly:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</>
);
}
I can't see any difference in behaviour and can't imagine case when count will not be updated (or will not be the most recent). Because whenever count is changing, new closure for onClick will be called, capturing the most recent count.
State update is asynchronous in React. So it is possible that there would be old value in count when you're updating it next time. Compare, for example, result of these two code samples:
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => {
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1)}
}>+</button>
</>
);
}
and
function Counter({initialCount}) {
const [count, setCount] = useState(initialCount);
return (
<>
Count: {count}
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => {
setCount(count + 1);
setCount(count + 1)}
}>+</button>
</>
);
}
I stumbled into a need for this recently. For example let's say you have a component that fills up an array with some amount of elements and is able to append to that array depending on some user action (like in my case, I was loading a feed 10 items at a time as the user kept scrolling down the screen. the code looked kind of like this:
function Stream() {
const [feedItems, setFeedItems] = useState([]);
const { fetching, error, data, run } = useQuery(SOME_QUERY, vars);
useEffect(() => {
if (data) {
setFeedItems([...feedItems, ...data.items]);
}
}, [data]); // <---- this breaks the rules of hooks, missing feedItems
...
<button onClick={()=>run()}>get more</button>
...
Obviously, you can't just add feedItems to the dependency list in the useEffect hook because you're invoking setFeedItems in it, so you'd get in a loop.
functional update to the rescue:
useEffect(() => {
if (data) {
setFeedItems(prevItems => [...prevItems, ...data.items]);
}
}, [data]); // <--- all good now
I have answered a similar question like this and it was closed because this was the canonical question - that i did not know of, upon looking the answers i decided to repost my answer here since i think it adds some value.
If your update depends on a previous value found in the state, then you should use the functional form. If you don't use the functional form in this case then your code will break sometime.
Why does it break and when
React functional components are just closures, the state value that you have in the closure might be outdated - what does this mean is that the value inside the closure does not match the value that is in React state for that component, this could happen in the following cases:
1- async operations (In this example click slow add, and then click multiple times on the add button, you will later see that the state was reseted to what was inside the closure when the slow add button was clicked)
const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
}}
>
immediately add
</button>
<button
onClick={() => {
setTimeout(() => setCounter(counter + 1), 1000);
}}
>
Add
</button>
</>
);
};
2- When you call the update function multiple times in the same closure
const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
setCounter(counter + 1);
}}
>
Add twice
</button>
</>
);
}
The “state update is asynchronous in React” answer is misleading, as are some comments below it. My thinking was also wrong until I dug into this further. You are right, this is rarely needed.
The key idea behind functional state updates is that state you depend on for the new state might be stale. How does state get stale? Let’s dispel some myths about it:
Myth: State can be changed under you during event handling.
Fact: The ECMAScript event loop only runs one thing at a time. If you are running a handler, nothing else is running alongside it.
Myth: Clicking twice fast (or any other user action happening quickly) can cause state updates from both handler calls to be batched.
Fact: React is guaranteed to not batch updates across more than one user-initiated event. This is true even in React 18, which does more batching than previous versions. You can rely on having a render in between event handlers.
From the React Working Group:
Note: React only batches updates when it’s generally safe to do. For example, React ensures that for each user-initiated event like a click or a keypress, the DOM is fully updated before the next event. This ensures, for example, that a form that disables on submit can’t be submitted twice.
So when do you get stale state?
Here are the main 3 cases I can think of:
Multiple state updates in the same handler
This is the case already mentioned where you set the same state multiple times in the same handler, and depend on the previous state. As you pointed out, this case is pretty contrived, because this clearly looks wrong:
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}
>+</button>
A more plausible case is calling multiple functions that each do updates on the same state and depend on the previous state. But that’s still weird, it’d make more sense to do all the calculations then set the state once.
Async state updates in a handler
For example:
<button
onClick={() => {
doSomeApiCall().then(() => setCount(count + 1));
}}
>+</button>
This is not so obviously wrong. The state can be changed in between you calling doSomeApiCall and when it resolves. In this case, the state update really is async, but you made it that way, not React!
The functional form fixes this:
<button
onClick={() => {
doSomeApiCall().then(() => setCount((currCount) => currCount + 1));
}}
>+</button>
Updating state in useEffect
G Gallegos's answer pointed this out for useEffect in general, and letvar's answer pointed this out for useEffect with requestAnimationFrame. If you're updating state based on previous state in useEffect, putting that state in the dependency array (or not using a dependency array) is a recipe for infinite loops. Use the functional form instead.
Summary
You don’t need the functional form for state updates based on previous state, as long as you do it 1. in a user-triggered-event handler 2. once per handler per state and 3. synchronously. If you break any of those conditions, you need functional updates.
Some people might prefer to always use functional updates, so you don’t have to worry about those conditions. Others might prefer the shorter form for clarity when it’s safe to do so, which is true for many handlers. At that point it’s personal preference / code style.
Historical note
I learned React before Hooks, when only class components had state. In class components, “multiple state updates in the same handler” doesn’t look so obviously wrong:
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}}
>+</button>
Since state is an instance variable instead of a function parameter, this looks fine, unless you know that setState batches calls when in the same handler.
In fact, in React <= 17, this would work fine:
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}, 1000);
Since it’s not an event handler, React re-renders after each setState call.
React 18 introduces batching for this and similar cases. This is a useful performance improvement. There is the downside that it breaks class components that rely on the above behavior.
References
React Working Group discussion
ehab’s answer, which also mentions the two cases where functional updates are needed.
Another use case for using functional updates with setState - requestAnimationFrame with react hooks. Detailed information is available here - https://css-tricks.com/using-requestanimationframe-with-react-hooks/
In summary, handler for requestAnimationFrame gets called frequently resulting in incorrect count value, when you do setCount(count+delta). On the other hand, using setCount(prevCount => prevCount + delta) yields correct value.
Normally when we need to update a state in a functional component, we do something like this:
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>);
}
When and why will we ever need to use the functional update form?
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>You clicked {count} times</p>
<button onClick={() => setCount(c=>c + 1)}>
Click me
</button>
</div>);
}
Use the function form when the setter may close over an old state value.
For example, if an async request is initiated, and you want to update state after that's done, the request that was made will have scope of the state as it was at the beginning of the request, which may not be the same as the most up-to-date render state.
You may also need to use the function form if the same state value was just updated, eg
setValue(value + 1);
// complicated logic here
if (someCondition) {
setValue(value => value + 1);
}
because the second call of setValue closes over an old value.
State Updates May Be Asynchronous:
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
useState is the same as setState in this condition.
You can see the different when call set state twice:
<button
onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}
></button>;
<button
onClick={() => {
setCount(c => (c + 1));
setCount(c => (c + 1));
}}
></button>;
There are other use cases too. For example, when you call useState inside an effect. If new state is dependent on old state, this might cause an infinite loop.
useEffect(() => {
setCounter(counter + 1);
}, [counter]);
You can avoid this by using functional updates:
useEffect(() => {
setCounter(old => old + 1);
}, []);
According to this: https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
The the functional update form make sure that the previous state that you take reference from is the latest / finalized version when there might be multiple setState hook (which is asynchronous) called (for example, if the user spam click on the button)
And also due to its async nature, the state will not be updated right away within a function, for e.g:
func() {
console.log(counter) // counter = 1
setCounter(counter => counter + 1) // counter = 1
console.log(counter) // counter = 1
}
The functional update form also allows the update function to be passed to its children while still having access to the parent’s state.
function MyButton(props){
// return <button onClick={()=>props.onClick(count+1)}>+1</button>; // error as count is not exposed here
return <button onClick={()=>props.onClick(n=>(n+1))}>+1</button>;
}
function Example() {
const [count, setCount] = React.useState(0);
return (<div><p>Counter: {count}</p>
<MyButton onClick={setCount}/>
</div>);
}
ReactDOM.render(<Example/>,document.querySelector("div"));
Code:
useEffect(() => {
setTimeout(() => {
// this cause re-render twice
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}, 1000);
}, []);
My question is why this component re-render twice if we call two setCount in sequence.
Doesn't React batch multiple setCount in one go?
Thank you for answering or any suggestion.
codesandbox example
Doesn't React batch multiple setCount in one go?
It does if it can. This is a case where it can not.
In order for react to batch things, the execution of code needs to start within react itself. For example, any component lifecycle events, or any synthetic event callbacks (eg, the onClick for a <button>). When that happens, react can let your code keep running, queuing up as many set states as you like, and then once your done you will return to react's code, which can then render the queued changes.
But if code execution did not start from react, then once you return, you're not going to be returning to react's code. So since they won't get to run code after yours, they do the render right away.
React will batch state updates if they're triggered from within a React-based event, like from inside a event handler or if they are called synchronously. It will not batch updates if they're triggered outside of React, like a setTimeout() or a Promise callback.
In your example the setCount is called from the context of a timer hence the batch updates did not happen. But if you call setCount multiple times from the context of a event handler or called synchronously, the state updates will be batched as shown in the snippet below.
Notice that the timer and the promise callbacks do not batch the updates:
function App() {
const [count, setCount] = React.useState(0);
console.log("re-render");
React.useEffect(() => {
setTimeout(() => {
// This cause re-render twice
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}, 2000);
// Will be batched as it it called
// In the context of React
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
//The state updates from the promise callback
//Will not be batched
Promise.resolve(1).then(data => {
console.log("Promise Resolved");
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
});
}, []);
const clickHandler = () => {
// Will be batched
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};
return (
<div className="App">
<h1>{count}</h1>
<button onClick={clickHandler}>Click</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
The other answers explained why it is rendering several times.
Now, I want to give a solution to make it render only once. There is an API that allows you to make only one render: ReactDOM.unstable_batchedUpdates
In your example:
ReactDOM.unstable_batchedUpdates(() => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
});
}, 1000);
https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#automatic-batching
Now, it's something React handles itself.
This question already has answers here:
Why React useState with functional update form is needed?
(5 answers)
When are functional updates required for computations involving previous state?
(1 answer)
Closed 2 years ago.
we are talking about functional components having useState
lets say
const [age, setAge] = useState(0)
now let us say while updating age I have to use the previous age
React docs mention something called FUNCTIONAL UPDATES where you can pass a function and the argument to that will be the previous value of the state, eg.
setState((previousAge) => previousAge + 1)
why do I need to do this when I can just do
setState(previousAge + 1)
what are the benefits of using functional setState,
I know in class-based components there was something called batching of state updates in the functional way,
but I can't find anything like that in functional components documentation.
They are not the same, if your update depends on a previous value found in the state, then you should use the functional form. If you don't use the functional form in this case then your code will break sometime.
Why does it break and when
React functional components are just closures, the state value that you have in the closure might be outdated - what does this mean is that the value inside the closure does not match the value that is in React state for that component, this could happen in the following cases:
1- async operations (In this example click slow add, and then click multiple times on the add button, you will later see that the state was reseted to what was inside the closure when the slow add button was clicked)
const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
}}
>
immediately add
</button>
<button
onClick={() => {
setTimeout(() => setCounter(counter + 1), 1000);
}}
>
Add
</button>
</>
);
};
2- When you call the update function multiple times in the same closure
const App = () => {
const [counter, setCounter] = useState(0);
return (
<>
<p>counter {counter} </p>
<button
onClick={() => {
setCounter(counter + 1);
setCounter(counter + 1);
}}
>
Add twice
</button>
</>
);
}
Problems might occur depending on how fast/often your setter gets called.
If you are using the simple way by getting the value from the closure, subsequent calls between two renders might not have the desired effect.
A simple example:
function App() {
const [counter, setCounter] = useState(0);
const incWithClosure = () => {
setCounter(counter + 1);
};
const incWithUpdate = () => {
setCounter(oldCounter => oldCounter + 1);
};
return (<>
<button onClick={_ => { incWithClosure(); incWithClosure(); }}>
Increment twice using incWithClosure
</button>
<button onClick={_ => { incWithUpdate(); incWithUpdate(); }}>
Increment twice using incWithUpdate
</button>
<p>{counter}</p>
</>);
}
Both buttons calls one of the increment methods twice.
But we observe:
The first button will increment the counter only by 1
The second button will increment the counter by 2, which is probably the desired outcome.
When can this happen?
Obviously, if incWithClosure is called multiple times immediately after each other
If asynchronous tasks are involved, this can easily happen (see below)
Perhaps, if React has much work to do, its scheduling algorithms may decide to handle multiple very fast clicks using the same event handler
Example with asynchronous work (simulating loading a resource):
function App() {
const [counter, setCounter] = useState(0);
const incWithClosureDelayed = () => {
setTimeout(() => {
setCounter(counter + 1);
}, 1000);
};
const incWithUpdateDelayed = () => {
setTimeout(() => {
setCounter((oldCounter) => oldCounter + 1);
}, 1000);
};
return (
<>
<button onClick={(_) => incWithClosureDelayed()}>
Increment slowly using incWithClosure
</button>
<button onClick={(_) => incWithUpdateDelayed()}>
Increment slowly using incWithUpdate
</button>
<p>{counter}</p>
</>
);
}
Click on the first button twice (within one second) and observe that the counter gets incremented only by 1. The second button has the correct behavior.
Because if you don't you will find at some point that you get an old value for age. The problem is, sometimes what you suggest will work. But sometimes it will not. It may not break in your current code today but it may break in a different code you wrote a few weeks ago or your current code a few months from now.
The symptom is really, really weird. You can print the value of the variable inside a jsx component using the {x} syntax and later print the same variable using console.log after rendering the jsx component (not before) and find that the console.log value is stale - the console.log that happens after the render can somehow have older value than the render.
So the actual value of state variables may not always work correctly in regular code - they are only designed to return the latest value in a render. For this reason the callback mechanism in a state setter was implemented to allow you to get the latest value of a state variable in regular code outside of a render.
I have a search input in one of my page and I'm trying to make sure that it does not make 10 requests in one second if the user types too fast.
Seems like debouncing is the way to go. I've read multiple blog posts and SO questions. I'm trying to use debounce from lodash. I must be doing something wrong since what happens is that all my call are passed, just later.
Here is my component code:
const Partners = (props: any) => {
const [query, setQuery] = useState("");
const { partners, totalPages, currentPage } = props.partnersList;
useEffect(() => {
props.getPartners();
}, []);
useEffect(() => debounce(() => props.getPartners(query), 10000), [query]);
const handleQueryChange = (e: any) => {
setQuery(e.target.value);
};
const handlePageChange = (e: React.ChangeEvent<unknown>, value: number) => {
props.getPartners(query, value);
};
return (
<React.Fragment>
<Sidebar />
<MainContent className="iamSearchPartners">
<h1>Partners</h1>
<TextField
className="iamSearchPartners__search_input"
variant="outlined"
size="small"
onChange={handleQueryChange}
value={query}
/>
<PartnersList partners={partners} />
{totalPages > 1 && (
<Pagination
currentPage={currentPage || 1}
totalPages={totalPages}
handlePageChange={handlePageChange}
/>
)}{" "}
</MainContent>
</React.Fragment>
);
};
As you can see, I have a useEffect listening to changes to query.
debounce creates a debounced version of the function that passed as the argument. In this specific case, calling it in useEffect will create a new debounced version of the function on every render.
To mitigate this, the debounced version could be created outside of the render function, so it is not recreated on every render.
const myDebouncedFunction = debounce((handler, query) => handler(query), 10000);
const Partners = (props: any) => {
// omitted ...
useEffect(() => {
myDebouncedFunction(props.getPartners, query);
}, [query]);
// omitted ...
useMemo or putting it into a useState could work as well.
Another thing: useEffect only called the returned debounced version since it's an arrow function without braces, making it return the result of the evaluation. useEffect leverages the return of it's function to be some kind of "unsubscribe" handler, see React docs about "Effects with cleanup". So whenever query changed (after the second time), the effect was calling the function. The version above should be called on every query change.
Personally, I would try to handle the call of the debounced version of the function in the handleQueryChange instead of a useEffect, to make the "when should it happen" more clear.
I end up doing this manually honestly. Check out the differences between debouncing and throttling. I think you want to throttle the requests. If you debounce, nothing is going to happen until the timer ends.
If you want to wait 10 seconds as in your example, nothing should happen if the user types at least once every ten seconds, until 10 seconds after the last type. As opposed to throttling where a request would go out every 10 seconds.
This approach is kind of a hybrid because we are making sure the last one still goes out (as a debounce would), and throttling may not do, but we are still sending requests while the user is typing.
const timer = useRef<number>(0);
const lastEventSent = useRef(Date.now());
useEffect(() => {
if (Date.now() - lastEventSent.current < 500) {
// The event was fired too recently, but we still want
// to fire this event after the timeout if it is the last
// one (500ms in this case).
timer.current = setTimeout(() => {
lastEventSent.current = Date.now();
props.getPartners(query)
}, 500);
} else {
// An event hasn't been fired for 500ms, lets fire the event
props.getPartners(query)
lastEventSent.current = Date.now();
// And if the user was typing, there is probably a timer, lets clear that
clearTimeout(timer.current);
}
// Cleanup the timer if there was one and this effect is reloaded.
return () => clearTimeout(timer.current);
}, [query]);