Debounce with useCallback in React - javascript

I have a function that I'd like to debounce in my React project, using Lodash's debounce library.
The high level structure is like so (greatly simplified for purposes of this question):
I have a function that can be called multiple times, but should only trigger callApiToSavetoDatabase() once every 3 seconds.
const autoSaveThing = useRef(debounce(() => {
callApiToSaveToDatabase();
}, 3000)).current;
This API calls an asynchronous function that sets react State and calls an API.
const callApiToSaveToDatabase = useCallback(async () => {
console.log('Started API function');
setSomeState(true);
try {
const response = await apiCall(data);
} catch {
// failure handling
}
}, [ /* some dependencies */ ]);
What works:
callApiToSavetoDatabase() is correctly only called once during the debounce period.
What doesn't work:
We hit the console.log line in callApiToSavetoDatabase() but from debugging in the browser, the code quits out of callApiToSavetoDatabase() as soon as I set state with setSomeState(true).
Is there some limitation with setting state inside a useCallback function I'm hitting here?
It's worth noting that if I call callApiToSavetoDatabase() directly it works perfectly.

The issue here ended up being that my callApiToSaveToDatabase() function was both:
Inside my React component and
Also setting React state
This combination cause a component re-render which subsequently halted my function.
The solution here was to move callApiToSaveToDatabase() outside of the React component, and pass to it all the necessary state variables and setState function references it needed. An example of this is as follows:
// function that calls my API
const callApiToSaveToDatabase = async (someState,setSomeSTate) => {
setSomeState(true);
try {
const response = await apiCall(someState);
} catch {
// failure handling
}
};
// debounce wrapper around the API function above
const autoSaveThing = debounce((someState,setSomeState) => callApiToSaveToDatabase(someState,setSomeState), 3000));
// my React component
const myComponent = () => {
// some code
autoSaveThing(someState,setSomeState);
}

Related

React component does not re-render under Jest on state change

Component:
const MyComponent = props => {
const {price} = props;
const result1 = useResult(price);
return (
<div>...</div>
)
}
Custom Hook:
export const useResult = (price) => {
const [result, setResult] = useState([]);
useEffect(() => {
const data = [{price: price}]
setResult(data);
}, [price]);
return result;
};
Jest test:
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor(() => {
expect(...).toBeInTheDocument();
});
});
What it does happen with the above code is that MyComponent, when running the test, renders only once instead of two (when the application runs). After the initial render where result1 is an empty array, useEffect of useResult is running and since there is a state change due to setResult(data), I should expect MyComponent to be re-rendered. However, that's not the case and result1 still equals to [] whereas it should equal to [{price:300}].
Hence, it seems custom hooks under testing behave differently than the real app. I thought it would be okay to test them indirectly through the component that calls them.
Any explanation/thoughts for the above?
UPDATE
The issue that invoked the above erroneous behaviour was state mutation!! It worked with the app but not with the test! My mistake was to attempt to use push in order to add an element to an array that was a state variable...
Well, it seems that you are asking a very specific thing about testing a custom hook. In that case, I also had some issues in the past testing custom hooks through #testing-library and a different package was created (and recently incorporated into the #testing-library) that provides the renderHook() function for testing custom hooks. I suggest you to test that.
Original Package (do not use it. Use directly the TL one)
Docs about the renderHook() call inside the TL docs
You can read more about it in this blog post from Kent C. Dodds.
I also suggest you create a "state change" to test your component and test the hook with the renderHook().
Here is a simple codesandbox with some tests for a component similar to your case.
Original Answer
Essentially, your test is not waiting for the component to perform the side effects. There are 2 ways of waiting for that:
Using waitFor()
import { waitFor, screen } from '#testing-library/react'
// ...
// add the `async` before the callback function
it('should ...', async () => {
render(<MyComponent price={300}/>);
await waitFor(() =>
expect(screen.getByText('your-text-goes-here')).toBeInTheDocument()
)
});
Using the findBy* query from RTL, that returns a Promise (read the Docs here) and is a combination from the waitFor and getBy* query (read docs here)
import { screen } from '#testing-library/react'
// ...
// add the `async` before the callback function
it('should ...', async () => {
render(<MyComponent price={300}/>);
expect(await screen.findByText('your-text-goes-here')).toBeInTheDocument();
});
Step 1: the code being tested
If, as mentioned in the comments of the question, the operation inside the effect is synchronous, then using useEffect for setting this state based on the props is undesirable in all cases. Not only for testing.
The component will render, update the DOM and immediately need to re render the following frame because it's state was updated. It causes a flash effect for the user and needlessly slows the app down.
If the operation is cheap, it's way more efficient to just execute it on every render.
If the operation can be more expensive, you can wrap it in useMemo to ensure it only happens when there's changes to the inputs.
export const useResult = (price) => {
return useMemo(
// I assume this is a stub for a expensive operation.
() => [{price: price}],
[price]
);
};
If, for some obscure reason, you do need to do this in an effect anyway (you probably don't but there's edge cases), you can use a layoutEffect instead. It will be processed synchronously and avoid the flashing frame. Still wouldn't recommend it but it's a slight improvement over a regular effect.
Step 2: Testing
If you changed the component to not use an effect, it should now be correct from the first render, and you don't have the problem anymore. Avoiding having a problem in the first place is also a valid solution :D
If you do find the need to flush something synchronously in a test, there's now the flushSync function which does just that.
Perhaps it would also flush the state update in the effect, causing your test to work with no other changes. I guess it should, as new updates triggered by effects while flushing should continue to be processed before returning.
flushSync(() => {
render(
<MyComponent price={300}/>)
)
})
In any case there's no point doing this if you can instead improve the component to fix the additional render introduced by setting state in an effect.
you can do:
The test will have to be async: it('should ...', async() => { ....
await screen.findByText('whatever');
This is async so it will wait to find whatever and fail if it can't find it
or you can do
await waitFor (() => {
const whatever = screen.getByText('whatever');
expect(whatever).toBeInTheDocument();
})
You are not waiting for the component to be rerendered
import { waitFor, screen } from 'testing-library/react'
it('should ...', async () => {
render(
<MyComponent price={300}/>)
)
await waitFor (() => {
// check that props.price is shown
screen.debug() // check what's renderered
expect(screen.getByText(300)).toBeInTheDocument();
});
});

How to wait for state while setting consecutive states in react using UseState Hook

I am calling a function to handle the video play on click and there I want to set consecutive states using useState hook. But I want to wait for first one before cursor goes to next setState(useState Hook) without using useEffect hook to monitor it. This is a function just as an example.
const videoPlayHandler = () => {
setIsPaused(true); //wait for this one
setIsPlay(false); //run just after first state is done
}
In react 18, you can force it to do a synchronous rerender by using flushSync:
import { flushSync } from 'react-dom';
const videoPlayHandler = () => {
flushSync(() => {
setIsPaused(true);
});
setIsPlay(false);
}
I recommend reading the notes section on react's flushSync documentation
Note:
flushSync can significantly hurt performance. Use sparingly.
flushSync may force pending Suspense boundaries to show their fallback state.
flushSync may also run pending effects and synchronously apply any updates they contain before returning.
flushSync may also flush updates outside the callback when necessary to flush the updates inside the callback. For example, if there are pending updates from a click, React may flush those before flushing the updates inside the callback.
This doesn't answer your question, but in your case it would make more sense to just keep 1 state variable that determines if the video is playing or not.
const [isPlaying, setIsPlaying] = useState(false);
const videoPlayHandler = () => {
setIsPlaying(true);
}
const videoPauseHandler = () => {
setIsPlaying(false);
}

How to stop executing a command the contains useState?

This is my code which sends a GET request to my backend (mySQL) and gets the data. I am using useState to extract and set the response.data .
const baseURL = 'http://localhost:5000/api/user/timesheet/13009';
const [DataArray , setDataArray] = useState([]);
axios.get(baseURL).then( (response)=>{
setDataArray(response.data);
});
But useState keeps on sending the GET request to my server and I only want to resend the GET request and re-render when I click a button or execute another function.
Server Terminal Console
Is there a better way to store response.data and if not how can I stop automatic re-rendering of useState and make it so that it re-renders only when I want to.
As pointed out in the comments, your setState call is triggering a re-render which in turn is making another axios call, effectively creating an endless loop.
There are several ways to solve this. You could, for example, use one of the many libraries built for query management with react hooks, such as react-query. But the most straightforward approach would be to employ useEffect to wrap your querying.
BTW, you should also take constants such as the baseUrl out of the component, that way you won’t need to include them as dependencies to the effect.
const baseURL = 'http://localhost:5000/api/user/timesheet/13009';
const Component = () => {
const [dataArray , setDataArray] = useState([]);
useEffect(() => {
axios.get(baseURL).then( (response)=>{
setDataArray(response.data);
});
}, []);
// your return code
}
This would only run the query on first load.
you have to wrap your request into a useEffect.
const baseURL = 'http://localhost:5000/api/user/timesheet/13009';
const [DataArray , setDataArray] = useState([]);
React.useEffect(() => {
axios.get(baseURL).then((response)=>{
setDataArray(response.data);
})
}, [])
The empty dependency array say that your request will only be triggered one time (when the component mount). Here's the documentation about the useEffect
Add the code to a function, and then call that function from the button's onClick listener, or the other function. You don't need useEffect because don't want to get data when the component first renders, just when you want to.
function getData() {
axios.get(baseURL).then(response => {
setDataArray(response.data);
});
}
return <button onClick={getData}>Get data</button>
// Or
function myFunc() {
getData();
}

Is it not correct to do api call without useEffect?

I want to do an api call after a button is clicked in react.But I have read that to do async tasks, we use useEffect.
So is it okay to not use useEffect and do an api call without it?
I think that without using useEffect an api call would block the render.
useEffect runs depending on deps Array.
It is used to do async tasks.
But I want to do a api call onClick.So its not possible to use useEffect.
So,What is the correct way to do an api call if it has to be done on Click?
You can do api call with and w/o the useEffect, both are fine.
And no, you won't block the api call if you don't use useEffect.
const App = () => {
const makeApiCall = async () => {
// the execution of this function stops
// at this await call, but rest of the App component
// is still executes
const res = await fetch("../");
}
...
};

Do React hooks need to return a value?

I've recently started to build out custom hooks in my React application and have been following the documentation on the React website. However, the hooks which I am building require no return value as they set up data for Redux on initialization.
Example:
// custom hook
export const useSetup() {
useEffect(() => {
if (data) fetch().then(data => dispatch(setInit(data)))
}, [dispatch])
}
// functional component
export function Details() {
useSetup()
I can't find documentation explicitly stating that a hook needs to return anything. However, I cannot find an example of a hook not returning something. Can someone advise on if this approach is correct?
Yes, your approach is correct. React hooks are not required to return anything. The React documentation states that:
We don’t have to return a named function from the effect. We called it
cleanup here to clarify its purpose, but you could return an arrow
function or call it something different.
The return value of a function that is passed as an argument to a hook has a special use in the lifecycle of the React component it belongs to. Essentially, that return value is expected to be a function and executes before the component with the hook re-renders or is unmounted. React documentation call this kind of hook an "effect with cleanup."
The React documentation uses the example below to show what a useEffect hook looks like:
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
As you can see, the anonymous function that is used as an argument to useEffect does not have a return statement.
You can verify this by changing the function a little bit to log the return value:
const count = 0;
const a = () => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
}
console.log(a());
This prints undefined.
You can also use console.log on the useEffect function to see that it also returns undefined.
If you changed the hook to this:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
return () => {
console.log('cleanup');
}
});
You would see the "cleanup" message every time the component re-renders or is unmounted. You would have to trigger the re-render by updating the state of the component in some way.

Categories