custom hook -> useEffect -> while loop not breaking - reactjs

I try to create a custom hook for polling.
Problem:
My hook is using a while loop inside useEffect, but it seems that while loop is never breaking, even when I change the condition to false.
Code sandbox reproducing the problem:
https://codesandbox.io/s/clever-worker-jm1p5?file=/src/App.tsx
Code:
I have 2 files:
usePolling.ts (which is my hook)
App.tsx (where I am executing/calling the hook)
usePolling.ts
/* eslint-disable no-await-in-loop */
import { useState, useCallback, useEffect } from "react";
export interface PollingOptions<T> {
fetchFunc: () => Promise<T> | undefined;
}
export const usePolling = <T>(
{ fetchFunc }: PollingOptions<T>,
interval: number
) => {
const [data, setData] = useState<T>();
const [error, setError] = useState<Error>();
const [condition, setCondition] = useState(true);
const stopPolling = useCallback(() => {
setCondition(false);
}, []);
const performPolling = useCallback(async () => {
try {
const res = await fetchFunc();
setData(res);
} catch (err) {
setCondition(false);
setError(err);
return;
} finally {
await new Promise((r) => setTimeout(r, interval));
}
}, [fetchFunc, interval]);
useEffect(() => {
(async () => {
while (condition) {
await performPolling();
}
})();
return () => stopPolling();
}, [condition, performPolling, stopPolling, data]);
return { data, error, stopPolling };
};
App.tsx
import { useCallback, useMemo } from "react";
import { usePolling } from "./usePolling";
import "./styles.css";
export default function App() {
const fetchFunc = useCallback(async () => {
// please consider this as an API call
await new Promise((resolve) => setTimeout(resolve, 500));
return { isSigned: Math.random() < 0.5 };
}, []);
const { data, error, stopPolling } = usePolling({ fetchFunc }, 3000);
console.log(data, error, ">>>>>>>");
const isSigned = data?.isSigned;
const isContractSigned = useMemo(() => {
if (isSigned) stopPolling();
return isSigned || false;
}, [isSigned, stopPolling]);
return (
<div className="App">
<h1>{`Is contract signed: ${isContractSigned}`}</h1>
</div>
);
}
Why am I not using setInterval instead of while loop
If I use setInterval, and if I choose to change the polling interval to 100ms for example. There are 3 problems:
there is a very high chance of calling API before previous API call responds.
suppose for any reason 2nd API call responded before the first one, then I am in trouble
Unnecessary API calls.
If I use while loop instead of setInterval, then all the above mentioned problems are solved. Please note that I am still using setTimeout inside while loop, so it will wait for specified interval before the next iteration.
Expected solution:
Any solution that does not use setInterval is expected. If you suggest any improvements to the existing code (even not related to this specific problem), you are most welcome :)
Thank you!

The callback you pass as the first argument of useEffect assumes that the dependencies are constants.
You are starting a while loop with the condition being a constant. There is no way to stop that while loop execution.
If you change the condition, it's not going to stop the previous while loop, but start a new condition with the new value.
If the new value is false, it won't start so nothing would happen. But if it's true, then you've got 2 while loops running.
Check out the SWR, React Hooks for Data Fetching, it's probably exactly what you are looking for.
Update:
The reason why the while loop won't stop
When you have a while loop in normal code, you control it using a condition based on a variable.
The dependencies of a useEffect are passed as constants to the callback. So any loops running based on a state variable (truthy) in a useEffect will not terminate.
The can cleanup a setInterval because there is clearInterval function implemented in the browser, which takes an id, finds the reference of the interval, and stops it.
There is no clearWhileLoop as it's not feasible.
Whenever a dependency mutates, the cleanup function runs, but you can't do anything about a while loop in a cleanup function.
The next callback is fired with new constants.
Check out the docs for why useEffects run after each render

Related

Previous data showing even though cleaning up in useEffect

I have a component in my react native app that loads sessions related to a particular individual. In the useEffect() of that component I both load the sessions when the component comes into focus, and unload those sessions within the cleanup.
export const ClientScreen = (props) => {
const isFocused = useIsFocused();
const client = useSelector((state) => selectActiveClient(state));
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
return () => dispatch(unloadSessions()); // Cleaning up here...
};
loadSessions(props);
}
}, [isFocused, client?.id]);
const updatedProps = {
...props,
client,
};
return <ClientBottomTabNavigator {...updatedProps} />;
};
Generally the component is working as expected. However, I do notice that if I load the component with one client, then navigate away, and then come back to the component by loading a new client, that for a brief moment the sessions pertaining to the previous client show before being replaced the sessions relevant to the new client.
My question is, shouldn't the unloadVisits() that runs on cleanup -- which sets sessions to an empty array -- prevent this? Or is this some kind of react behavior that's holding onto the previous state of the component? How can I ensure this behavior doesn't occur?
Cleanup function should appear before the closing-brace of the useEffect hook
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
};
loadSessions(props);
}
return () => dispatch(unloadSessions()); // Cleaning up here... // <--- here
}, [isFocused, client?.id]);
as commented, your loadSessions returns a cleanup function, but you don't do anything with it. And the effect where you call loadSessions(props) does not return anything, that's why it does not clean up.
Edit:
I made a mistake, loadSessions returns a Promise of a cleanup function. And it is impossible to "unwrap" this Promise and get to the cleanup function itself in a way that you can return it in your effect. You have to move the cleaup function out of the async function loadSessions.
But you don't need async/await for everything:
useEffect(() => {
if (isFocused && client?.id) {
loadClientSessions(client.id).then(dispatch);
return () => dispatch(unloadSessions());
}
}, [isFocused, client?.id]);

useEffect dependency cause an infinite loop

function Reply({ id, user }) {
const [data, setData] = useState([]);
const [replyText, setReplyText] = useState("");
useEffect(() => {
async function fetchData() {
const response = await _axios.get("/reply/" + id);
setData(response.data);
}
fetchData();
}, [data]); <---- ** problem ** with data(dependency),
infinite request(call) fetchData()
...
}
what's the reason for infinite loop if there's a dependency.
as far as i know, when dependency(data) change, re-render.
but useEffect keep asking for data(axios.get(~~)).
if i leave a comment, i can see normally the latest comments, but the network tab(in develop tools) keeps asking for data(304 Not Modified, under image)
There's an infinite loop because that code says "If data changes, request information from the server and change data." The second half of that changes data, which triggers the first half again.
You're not using data in the callback, so it shouldn't be a dependency. Just remove it:
useEffect(() => {
async function fetchData() {
const response = await _axios.get("/reply/" + id);
setData(response.data);
}
fetchData();
}, []);
// ^^−−−−−−−−−− don't put `data` here
That gives you a blank dependency array, which will run the effect only when the component first mounts. (If you want to run it again after mount, use a different state member for that, or define fetchData outside the effect and use it both in the effect and at the other time you want to fetch data.)
Side note: Nothing in your code is handling rejections from your fetchData function, which will cause "unhandled rejection" errors. You'll want to hook up a rejection handler to report or suppress the error.
You are using setData after the response which causes the data to change and hence the useEffect(() => {<>your code<>} ,[data]) to fire again.
use useEffect(() => {<>your code<>},[]) if you want to execute the AJAX call only once after component mounting
or
use useEffect(() => {<>your code<>}) without the dependency if you want to execute the AJAX call after the component mount and after every update
Dependencies argument of useEffect is useEffect(callback, dependencies)
Let's explore side effects and runs:
Not provided: the side-effect runs after every rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs after EVERY rendering
});
}
An empty array []: the side-effect runs once after the initial rendering.
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Runs ONCE after initial rendering
}, []);
}
Has props or state values [prop1, prop2, ..., state1, state2]: the side-effect runs only when any dependency value changes.
import { useEffect, useState } from 'react';
function MyComponent({ prop }) {
const [state, setState] = useState('');
useEffect(() => {
// Runs ONCE after initial rendering
// and after every rendering ONLY IF `prop` or `state` changes
}, [prop, state]);
}

Debounce or throttle with react hook

I need to make an api request when a search query param from an input fields changes, but only if the field is not empty.
I am testing with several answers found on this site, but can't get them working
Firstly this one with a custom hook
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
now in my component I do this
const debouncedTest = useDebounce(() => {console.log("something")}, 1000);
but this seems to gets called every rerender regardless of any parameter, and I need to be able to call it inside a useEffect, like this
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
which of course does not work
Another approach using lodash
const throttledTest = useRef(throttle(() => {
console.log("test");
}, 1000, {leading: false}))
But how would i trigger this from the useEffect above? I don't understand how to make this work
Thank you
Your hook's signature is not the same as when you call it.
Perhaps you should do something along these lines:
const [state, setState] = useState(''); // name it whatever makes sense
const debouncedState = useDebounce(state, 1000);
useEffect(() => {
if (debouncedState) functionCall(debouncedState);
}, [debouncedState])
I can quickly point out a thing or two here.
useEffect(() => {
if (query) {
useDebounce(() => {console.log("something")}, 1000);
} else {
//...
}
}, [query]);
technically you can't do the above, useEffect can't be nested.
Normally debounce isn't having anything to do with a hook. Because it's a plain function. So you should first look for a solid debounce, create one or use lodash.debounce. And then structure your code to call debounce(fn). Fn is the original function that you want to defer with.
Also debounce is going to work with cases that changes often, that's why you want to apply debounce to reduce the frequency. Therefore it'll be relatively uncommon to see it inside a useEffect.
const debounced = debounce(fn, ...)
const App = () => {
const onClick = () => { debounced() }
return <button onClick={onClick} />
}
There's another common problem, people might take debounce function inside App. That's not correct either, since the App is triggered every time it renders.
I can provide a relatively more detailed solution later. It'll help if you can explain what you'd like to do as well.

React setState with callback in functional components

I have a very simple example I wrote in a class component:
setErrorMessage(msg) {
this.setState({error_message: msg}, () => {
setTimeout(() => {
this.setState({error_message: ''})
}, 5000);
});
}
So here I call the setState() method and give it a callback as a second argument.
I wonder if I can do this inside a functional component with the useState hook.
As I know you can not pass a callback to the setState function of this hook. And when I use the useEffect hook - it ends up in an infinite loop:
So I guess - this functionality is not included into functional components?
The callback functionality isn't available in react-hooks, but you can write a simple get around using useEffect and useRef.
const [errorMessage, setErrorMessage] = useState('')
const isChanged = useRef(false);
useEffect(() => {
if(errorMessage) { // Add an existential condition so that useEffect doesn't run for empty message on first rendering
setTimeout(() => {
setErrorMessage('');
}, 5000);
}
}, [isChanged.current]); // Now the mutation will not run unless a re-render happens but setErrorMessage does create a re-render
const addErrorMessage = (msg) => {
setErrorMessage(msg);
isChanged.current = !isChanged.current; // intentionally trigger a change
}
The above example is considering the fact that you might want to set errorMessage from somewhere else too where you wouldn't want to reset it. If however you want to reset the message everytime you setErrorMessage, you can simply write a normal useEffect like
useEffect(() => {
if(errorMessage !== ""){ // This check is very important, without it there will be an infinite loop
setTimeout(() => {
setErrorMessage('');
}, 5000);
}
}, [errorMessage])

React hook missing dependency

I'm hoping someone can explain to me the correct usage of React hook in this instance, as I can't seem to find away around it.
The following is my code
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// This is a trick so that the debounce doesn't run on initial page load
// we use a ref, and set it to true, then set it to false after
const firstUpdate = React.useRef(true);
const UserSearchTimer = React.useRef()
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
function _debounceSearch() {
clearTimeout(UserSearchTimer.current);
UserSearchTimer.current = setTimeout( async () => {
_getUsers();
}, DEBOUNCE_TIMER);
}
async function _getUsers(query = {}) {
if(type) query.type = type;
if(search) query.search = search;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
So essentially I have a table in which i am displaying users, when the page changes, or the perPage, or the order, or the type changes, i want to requery my user list so i have a useEffect for that case.
Now generally I would put the _getUsers() function into that useEffect, but the only problem is that i have another useEffect which is used for when my user starts searching in the searchbox.
I don't want to requery my user list with each and every single letter my user types into the box, but instead I want to use a debouncer that will fire after the user has stopped typing.
So naturally i would create a useEffect, that would watch the value search, everytime search changes, i would call my _debounceSearch function.
Now my problem is that i can't seem to get rid of the React dependency warning because i'm missing _getUsers function in my first useEffect dependencies, which is being used by my _debounceSearch fn, and in my second useEffect i'm missing _debounceSearch in my second useEffect dependencies.
How could i rewrite this the "correct" way, so that I won't end up with React warning about missing dependencies?
Thanks in advance!
I would setup a state variable to hold debounced search string, and use it in effect for fetching users.
Assuming your component gets the query params as props, it would something like this:
function Component({page, perPage, order, type, search}) {
const [debouncedSearch, setDebouncedSearch] = useState(search);
const debounceTimer = useRef(null);
// debounce
useEffect(() => {
if(debounceTime.current) {
clearTimeout(UserSearchTimer.current);
}
debounceTime.current = setTimeout(() => setDebouncedSearch(search), DEBOUNCE_DELAY);
}, [search]);
// fetch
useEffect(() => {
async function _getUsers(query = {}) {
if(type) query.type = type;
if(debouncedSearch) query.search = debouncedSearch;
if(order.orderBy && order.order) {
query.orderBy = order.orderBy;
query.order = order.order;
}
query.page = page+1;
query.perPage = perPage;
setLoading(true);
try {
await get(query);
}
catch(error) {
console.log(error);
props.onError(error);
}
setLoading(false);
}
_getUsers();
}, [page, perPage, order, type, debouncedSearch]);
}
On initial render, debounce effect will setup a debounce timer... but it is okay.
After debounce delay, it will set deboucedSearch state to same value.
As deboucedSearch has not changed, ferch effect will not run, so no wasted fetch.
Subsequently, on change of any query param except search, fetch effect will run immediately.
On change of search param, fetch effect will run after debouncing.
Ideally though, debouncing should be done at <input /> of search param.
Small issue with doing debouncing in fetching component is that every change in search will go through debouncing, even if it is happening through means other than typing in text box, say e.g. clicking on links of pre-configured searches.
The rule around hook dependencies is pretty simple and straight forward: if the hook function use or refer to any variables from the scope of the component, you should consider to add it into the dependency list (https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies).
With your code, there are couple of things you should be aware of:
1.With the first _getUsers useEffect:
useEffect(() => {
_getUsers()
}, [page, perPage, order, type])
// Correctly it should be:
useEffect(() => {
_getUsers()
}, [_getUsers])
Also, your _getUsers function is currently recreated every single time the component is rerendered, you can consider to use React.useCallback to memoize it.
2.The second useEffect
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [search])
// Correctly it should be
useEffect(() => {
if(firstUpdate.current)
firstUpdate.current = false;
else
_debounceSearch()
}, [firstUpdate, _debounceSearch])

Resources