I have a react antd component that uses the onMouseEnter prop that calls a query to my api. I want to make it so that when onMouseEnter first calls the api its works fine...then if the user somehow keeps moving the mouse over the component, not to call the api that many times. Maybe a timer between consecutive calls to the api so the network doesnt get flooded with calls.
This is the antd component very simple:
<Select onMouseEnter={handleMouseEnter} />
This is the function executed everytime the onMouseEnter is called:
const handleMouseEnter = () => {
refetchQueries();
}
Basically dont do consectuive calls if the user accidentally enters the component 5 times a couple seconds. And Im not sure if debounce would work here because from what I understood debounce calls the function x many seconds after the last time it was invoked.
You can create a new boolean state and set this state when onMouseEnter triggered and with setTimeout you can reset this state to initial condition and then you can use this state like condition to trigger onMouseEnter event like this
<Select onMouseEnter={ () => { this.state.isNotActive && {
setState({isNotActive: false});
handleMouseEnter;
setTimeout(() => { setState({isNotActive: true})}, 5000);
} } } />
Related
I am using setInterval inside useEffect with an empty dependency array inside one of my react components.
There is true/false useState which controls the display of that component.
When the state is false, the component is hidden and when it's true the component is shown.
Something like this:
const [state, setState] = useState(false)
// in jsx render section
return (
<div>
{state? <component/> : '' }
</div>
)
When the component loads for the first time the setInterval runs only one time which is what it supposes to do.
If the state goes to false then back to true, the component is removed from the UI and is then displayed back again. However, what happens here is now I have two setInterval functions running in the background, and the first one doesn't shutdown.
The number of the setInterval functions keeps increasing with each time that component re-render.
I don't understand how React works in this situation.
I need to know how it works (i.e. why won't the function shutdown, why are the number of functions increasing) and how to fix it.
This is the structure of React useEffect.React performs the cleanup when the component unmounts.
useEffect(() => {
//effect
return () => {
//cleanup runs on unmount
}
}, [])
The cleanup function should have clearInterval() which will basically removes setInterval or stops it when component unmounts. See the practical code below:
let intervalid;
useEffect(
() => {
intervalid = setInterval(() => {console.log("Iterate");}, 1000));
return function cleanup() {
console.log("cleaning up");
clearInterval(intervalid);
};
},
[]
);
This above code is just for understanding approach. Every effect may return a function that cleans up after it. This lets us keep the logic for adding and removing subscriptions close to each other. # FROM REACT DOCS Reference
This is a classic menu case, open on a button click, hide in 5 seconds if no activity. We have 2 state variables. open and active, corresponding to two different states = menu is open, and active (being used). I set the open variable using a button click, then in effect I start the 5 second timeout. Now if the user mouseovers the menu,I set the active property to true and try to clear the timeout. But that is not working. Meaning, the timeout var is always null inside the code that is supposed to clear it.
Here is some code to help you understand:
let [open, openMenu] = useState(false)
let [active, activateMenu] = useState(false)
let timer = null;
useEffect(() => {
if(open && !active) {
timer = setTimeout(() => setOpen(false), 5000)
}
if(open && active) {
if(timer) {
clearTimeout(timer)
}
}
}, [open, active])
// Triggered via the UI on a button click
openMenuhandler() {
setOpen(true)
}
// Triggered via the UI on mouseenter
// identifies that the menu is being used, when mouse is over it
setMenuActive() {
activateMenu(true)
}
// Triggered via the UI on mouseleave
setMenuInActive() {
activateMenu(false)
}
// other code here on
Now the timeout is never cleared. The menu hides in 5 seconds, no matter what. Is there another way to do it? I have even tried moving the clearTimeout to the mouseLeave, but even then the timer is null. How to fix this? Please help.
Whenever your component re-renders, timer variable will be re-declared with an initial value of null. As a result, when useEffect hook executes whenever any of its dependency changes, timer variable is null.
You can solve the problem by making sure that value of the timer variable is persisted across re-renders of your component. To persist the value across re-renders, either save the id of the setTimeout using useRef hook or save it in the state, i.e. useState hook.
I'm writing tests for a React component. It's a timer that starts counting down when you press a button, then stops when you press the same button. I have a test case that tries to press the pause button, wait a second, then press the pause button again, checking the timer to make sure that a second has elapsed:
Timer.test.js
render(<Timer />)
const pauseButton = screen.getByText('pause')
const timerOutput = screen.getAllByRole('heading')[1]
describe('Timer', () => {
test('Timer starts counting down when unpaused', done => {
function fetchTime(callback) {
fireEvent.click(pauseButton)
setTimeout(
fireEvent.click(pauseButton),
1250
)
return callback(timerOutput)
}
function callback(data) {
try {
expect(data).toHaveTextContent('24:59')
done()
} catch(error) {
done(error)
}
}
fetchTime(callback)
})
})
The problem is, the test doesn't seem to be hitting click on pauseButton the way I want it to. Jest tells me in the terminal when I run my test that timerOutput turns out to be '25:00' rather than '24:59', and it seems as if the component failed the test. But this is a problem with the test, not the component; when I run the app in my browser and press the button myself, it works the way it should. How do I get this test to work properly, and hit the buttons the way I want it to?
It's difficult to have an accurate answer with few information about the component itself.
First I would recommend use async arrow function inside test() whenever you need to handle async calls so you don't rely in callback hell.
Besides that, I would try to use jest.useFakeTimers() so you can advance the setTimeout timer in order to test properly. It seems that your second fireEvent.click never gets fired since the test checks it synchronously.
And I just noticed you requested the timerOutput at first but didn't request it after the click events.
I would suggest something like:
test('Timer starts counting down when unpaused', async () => {
jest.useFakeTimers();
fireEvent.click(pauseButton)
setTimeout(
() => {fireEvent.click(pauseButton)},
1250
)
jest.runPendingTimers(); // This would run the above function
expect(screen.getAllByRole('heading')[1]).toHaveTextContent('24:59')
}
})
Indeed, the expect statement would be better from a user perspective assertion, like:
expect(screen.getByText("24:59")).toBeVisible();
Since you don't matter about the HTML elements that contains that text content
I'm relatively new to React & Typescript/JS and am trying to understand how to debounce a call to a promise properly. There's a button to increment/decrement a desired purchase quantity and only after a certain period of time do I want to actually execute the call to the backend API, to avoid the user just repeatedly hitting the +/- buttons and spamming the server.
import debounce from 'lodash/debounce'
const [isQueryingPrice, setIsQueryingPrice] = useState(false);
const [desiredPurchaseQty, setDesiredPurchaseQty] = useState<number>(1);
const changeQty = (delta: number) => {
setDesiredPurchaseQty(desiredPurchaseQty + delta);
setIsQueryingPrice(true);
debounce(fetchPrice, 1000)();
}
const incrementQty = (event: any) => {
changeQty(1);
}
const decrementQty = (event: any) => {
changeQty(-1);
}
const fetchPrice = (qty: number) => {
// not sure that arrow function wrapping fetch is the best approach here?
// some imaginary api
fetch("https://api.example.com/get-price?qty=" + qty).then((response) => {
return response.json();
});
}
// in my component:
< div>
<Button onClick={decrementQty}>Dec</Button>
<Button onClick={incrementQty}>Inc</Button>
</div>
However, whilst the increment/decrement works, the debounced function is fired multiple times when the timer expires. I want to have a single promise executed when the user has stopped pressing the change quantity buttons, and I want the promise execution to respect the count, i.e. if the user presses the count button 3 times, such that the total is 4, then the call to get the price should be for a quantity of 4.
I think this might be that the calls to change state cause a re-render and a creation of a new debounced function and I think perhaps the calls to the debounce function don't cause the old scheduled callbacks to be cleared. However, this is where my knowledge of JS starts to break down...
I'd also like to know how the approach should work for an initial load of price: I think there's a call inside a useEffect() to get the price when the page first loads.
You don't want to create a new debounced function every time you change quantity and certainly not on every render.
Here I've wrapped the original fetchPrice with the debounce function and also saved state so we can use the same debounced function across renders.
const [debouncedFetchPrice] = useState(() => debounce(fetchPrice, 1000));
and then in changeQty function you should call debouncedFetchPrice instead of fetchPrice
This should mean it will trigger with the latest desiredPurchaseQty value after a 1000 millis delay.
Furthermore...
I've found this article useful in the past to create a useDebounce hook with useEffect. Check it out
https://dev.to/gabe_ragland/debouncing-with-react-hooks-jci
I'am puzzled by the setState() accepted object.The code link is here https://codepen.io/DRL9/pen/jadbWq and the code is as follow:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
intervalCount: 1,
buttonCount: 1
};
this.increment = 1;
this.intervalId = null;
}
tick() {
this.setState({
intervalCount: this.state.intervalCount + this.increment
});
this.setState({
intervalCount: this.state.intervalCount + this.increment
});
}
onClick() {
this.setState({
buttonCount: this.state.buttonCount + this.increment
});
this.setState({
buttonCount: this.state.buttonCount + this.increment
});
}
componentDidMount() {
this.intervalId = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.intervalId);
}
render() {
return <div>
<div>
interval counter: {this.state.intervalCount}
</div>
<button onClick={this.onClick.bind(this)}>increment</button>
<div>
button counter: {this.state.buttonCount}
</div>
</div>;
}
}
I expect that intervalCount will increment 1 like the behavior when I click increment button. However, it increment 2 each tick.
The only different is that one updated is in setInterval function and the other updated is in onClick function.
Why are their behavior different?
We can't talk in absolutes regarding the timing of setState as it is, by definition, unpredictable. The state changes may be delayed to some time in the future, and this behavior may be different depending on the version of React that you are using.
In the example provided, React is delaying state updates until the onClick handler has finished running. React knows when this handler is finished running because we are passing the handler through JSX's onClick (which is then processed internally by React):
// React processes the onClick handler below
<button id="btn" onClick={this.onClick.bind(this)}>increment</button>
If we were to instrument the onClick logic ourselves, by manually grabbing the button element from the DOM and adding a click event listener that calls our onClick handler, the button updates identically to the setInterval (React doesn't know that we are updating state within a click handler, so it chooses not to make the optimization of batching the calls to setState).
See this codepen, where the button counter has a click handler manually added to it in the componentDidMount function as opposed to using JSX's onClick. Notice that the button counter now increments in intervals 2 instead of 1.
I want to stress that this behavior is not deterministic and you should never use this.state within your setState function. Instead, you want to use the variation of setState that accepts an updater function that contains the previous state. Then, build your new state from the state passed to the updater:
this.setState(state => ({
buttonCount: state.buttonCount + this.increment
}));
See this codepen, which uses an updater to update the button counter, producing the expected effect of updating the button counter in intervals of 2.
For more info on setState see the official documentation.
From the documentation for setState:
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
This is saying that when you reference state data (this.state.buttonCount or this.state.intervalCount) immediately after you've changed it using setState (as you do in both functions on the second setState command) the behavior will be unpredictable. Maybe setState immediately updates the state data, as it seems to be doing with intervalCount, and maybe setState waits to update the state data so it can batch it later, as it seems to be doing with buttonCount. As a developer we should avoid exposing ourselves to such unpredictable behavior by using other variables when we want to modify the state multiple times during the same event.
As to why intervalCount is fairly consistently being updated immediately (and thus incrementing the second time) and buttonCount is consistently being batched (and only incrementing one time for the two calls to setState) my guess is this: onClick is triggered by a user interaction so the React engine probably guesses that during user interactions a lot of state may be changing, so it batches the calls to setState, maybe until the event fully propagates. tick, on the other hand, is triggered by an internal callback without any user interaction being processed, so the React engine probably guesses it's safe to update state right away without batching.