how to unmount a function in useEffect (react js) - reactjs

test function dose not unmount and wen i click on correectAnswer the last function (test) is steal running and again test function will run and then when the last test function achieve to 0 we go to loser page.
const [state, setState] = useState({
haveTime: 10
})
const [states] = useState({
correct: "question",
step: "loser"
})
const test = (timer) => {
let haveTime = 10
let time = setInterval(() => {
haveTime -= 1;
setState({ haveTime })
// console.log(state.haveTime)
}, 1000);
setTimeout(() => {
clearInterval(time)
dispatch(getNameStep(states.step))
}, timer);
}
const correectAnswer = () => {
if (index === 9) {
dispatch(getNameStep(stateForWinner.step))
}
else {
dispatch({
type: "indexIncrease"
})
test(10000)
}
}
let { question, correct_answer } = details.question[index];
useEffect(() => {
test(10000)
}, [])

There are a few things wrong with your code.
First you are combining setInterval with setTimeout which is not a good idea just because of the amount of coordination that needs to happen.
Second to clear an interval or a timeout you need to do it from within the useEffect by returning a function.
Third you have no "dependencies" in your useEffect.
Look at this code that use in one my apps:
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
SetSearchFilter(SearchLocal, State.Reversed);
}, 250)
// this is how you clear a timeout from within a use effect
// by returning a function that does the disposing
return () => clearTimeout(delayDebounceFn);
}, [SearchLocal]);//here you need to add the actual dependencies of your useEffect
Lastly you need to breakdown your useEffect to perform a "single effect". Combining "too much stuff" into a single use effect is not good because then it is very difficult to debug and to achieve what you want.
You need to break down your useEffect into smaller useEffects.
You need to tell the useEffect when you want it to run by adding the dependencies. This way you know that a particular useEffect will run for ecxample if the "nextStep" has changed or if the test has reached the end.

Related

How to use context values in useEffect, that only runs once

i've got an interesting problem here. I am building a react application using web socket communication with the server. I create this websocket in a useEffect hook, which therefore cannot run multiple times, otherwise i'd end up with multiple connections. In this useEffect, however i intend to use some variables,which are actually in a context (useContext) hook. And when the context values change, the values in useEffect , understandably, don't update. I've tried useRef, but didn't work. Do you have any ideas?
const ws = useRef<WebSocket>();
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
ws.current.addEventListener("message", (message) => {
const messageData: ResponseData = JSON.parse(message.data);
const { response, reload } = messageData;
if (typeof response === "string") {
const event = new CustomEvent<ResponseData>(response, {
detail: messageData,
});
ws.current?.dispatchEvent(event);
} else {
if (reload !== undefined) {
console.log("general info should reload now");
GeneralInfoContext.reload(reload);
}
console.log(messageData);
}
});
});
The web socket is stored as a ref for better use in different functions outside of this useEffect block
Note: the context value to be used is actually a function, GeneralInfoContext.reload()
Solution with split useEffect
You can split the logic that opens the websocket connection vs. the one that adds the message handler into separate useEffects - the first can run once, while the second can re-attach the event every time a dependency changes:
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
}, []);
useEffect(() => {
const socket = ws.current;
if(!socket) throw new Error("Expected to have a websocket instance");
const handler = (message) => {
/*...*/
}
socket.addEventListener("message", handler);
// cleanup
return () => socket.removeEventListener("message", handler);
}, [/* deps here*/])
The effects will run in order so the second effect will run after the first effect has already set ws.current.
Solution with callback ref
Alternatively you could put the handler into a ref and update it as necessary, and reference the ref when calling the event:
const handlerRef = useRef(() => {})
useEffect(() => {
handlerRef.current = (message) => {
/*...*/
}
// No deps here, can update the function on every render
});
useEffect(() => {
ws.current = new WebSocket("ws://localhost:5000");
ws.current.addEventListener("open", () => {
console.log("opened connection");
});
const handlerFunc = (message) => handlerRef.current(message);
ws.current.addEventListener("message", handlerFunc);
return () => ws.current.removeEventListener("message", handlerFunc);
}, []);
It's important that you don't do addEventListener("message", handlerRef.current) as that will only attach the original version of the function - the extra (message) => handlerRef.current(message) wrapper is necessary so that every message gets passed to the latest version of the handler func.
This approach still requires two useEffect as it's best to not put handlerRef.current = /* func */ directly in the render logic, as rendering shouldn't have side-effects.
Which to use?
I like the first one personally, detaching and reattaching event handlers should be harmless (and basically 'free') and feels less complicated than adding an additional ref.
But the second one avoids the need for an explicit dependency list, which is nice too, especially if you aren't using the eslint rule to ensure exhaustive deps. (Though you definitely should be)
You can provide useEffect with a list of variables and useEffect will re-run when these variables change.
This is a little example:
const [exampleState, setExampleState] = useState<boolean>(false);
useEffect(() => {
console.log("exampleState was updated.");
}, [exampleState]);
An example from reactjs website:
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // Only re-subscribe if props.friend.id changes
You should pass an empty array as the second parameter to the useEffect, so it this case it becomes akin to the componentDidMount() logic of react
useEffect(() => {
...your websocket code here
}, [])

custom hook -> useEffect -> while loop not breaking

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

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 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])

react hooks and setInterval

Is there any alternative to just keeping a "clock" in the background to implement auto-next (after a few seconds) in carousel using react hooks?
The custom react hook below implements a state for a carousel that supports manual (next, prev, reset) and automatic (start, stop) methods for changing the carousel's current (active) index.
const useCarousel = (items = []) => {
const [current, setCurrent] = useState(
items && items.length > 0 ? 0 : undefined
);
const [auto, setAuto] = useState(false);
const next = () => setCurrent((current + 1) % items.length);
const prev = () => setCurrent(current ? current - 1 : items.length - 1);
const reset = () => setCurrent(0);
const start = _ => setAuto(true);
const stop = _ => setAuto(false);
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, 3000);
return _ => clearInterval(interval);
});
return {
current,
next,
prev,
reset,
start,
stop
};
};
There are differences between setInterval and setTimeout that you may not want to lose by always restarting your timer when the component re-renders. This fiddle shows the difference in drift between the two when other code is also running. (On older browsers/machines—like from when I originally answered this question—you don't even need to simulate a large calculation to see a significant drift begin to occur after only a few seconds.)
Referring now to your answer, Marco, the use of setInterval is totally lost because effects without conditions dispose and re-run every time the component re-renders. So in your first example, the use of the current dependency causes that effect to dispose and re-run every time the current changes (every time the interval runs). The second one does the same thing, but actually every time any state changes (causing a re-render), which could lead to some unexpected behavior. The only reason that one works is because next() causes a state change.
Considering the fact that you are probably not concerned with exact timing, is is cleanest to use setTimeout in a simple fashion, using the current and auto vars as dependencies. So to re-state part of your answer, do this:
useEffect(
() => {
if (!auto) return;
const interval = setTimeout(_ => {
next();
}, autoInterval);
return _ => clearTimeout(interval);
},
[auto, current]
);
Generically, for those just reading this answer and want a way to do a simple timer, here is a version that doesn't take into account the OP's original code, nor their need for a way to start and stop the timer independently:
const [counter, setCounter] = useState(0);
useEffect(
() => {
const id= setTimeout(() => {
setCounter(counter + 1);
// You could also do `setCounter((count) => count + 1)` instead.
// If you did that, then you wouldn't need the dependency
// array argument to this `useEffect` call.
}, 1000);
return () => {
clearTimeout(id);
};
},
[counter],
);
However, you may be wondering how to use a more exact interval, given the fact that setTimeout can drift more than setInterval. Here is one method, again, generic without using the OP's code:
// Using refs:
const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
() => {
const id = setInterval(() => {
r.current.setCounter(r.current.counter + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
// Using the function version of `setCounter` is cleaner:
const [counter, setCounter] = useState(30);
useEffect(
() => {
const id = setInterval(() => {
setCounter((count) => count + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
Here is what is going on above:
(first example, using refs): To get setInterval's callback to always refer to the currently acceptable version of setCounter we need some mutable state. React gives us this with useRef. The useRef function will return an object that has a current property. We can then set that property (which will happen every time the component re-renders) to the current versions of counter and setCounter.
(second example, using functional setCounter): Same idea as the first, except that when we use the function version of setCounter, we will have access to the current version of the count as the first argument to the function. No need to use a ref to keep things up to date.
(both examples, continued): Then, to keep the interval from being disposed of on each render, we add an empty dependency array as the second argument to useEffect. The interval will still be cleared when the component is unmounted.
Note: I used to like using ["once"] as my dependency array to indicate that I am forcing this effect to be set up only once. It was nice for readability at the time, but I no longer use it for two reasons. First, hooks are more widely understood these days and we have seen the empty array all over the place. Second, it clashes with the very popular "rule of hooks" linter which is quite strict about what goes in the dependency array.
So applying what we know to the OP's original question, you could use setInterval for a less-likely-to-drift slideshow like this:
// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...
const r = useRef(null);
r.current = { next };
useEffect(
() => {
if (!auto) return;
const id = setInterval(() => {
r.current.next();
}, autoInterval);
return () => {
clearInterval(id);
};
},
[auto]
);
Because the current value is going to change on every "interval" as long as it should be running, then your code will start and stop a new timer on every render. You can see this in action here:
https://codesandbox.io/s/03xkkyj19w
You can change setInterval to be setTimeout and you will get the exact same behaviour. setTimeout is not a persistent clock, but it doesn't matter since they both get cleaned up anyways.
If you do not want to start any timer at all, then put the condition before setInterval not inside of it.
useEffect(
() => {
let id;
if (run) {
id = setInterval(() => {
setValue(value + 1)
}, 1000);
}
return () => {
if (id) {
alert(id) // notice this runs on every render and is different every time
clearInterval(id);
}
};
}
);
So far, it seems that both solutions below work as desired:
Conditionally creating timer — it requires that useEffect is dependent both on auto and current to work
useEffect(
() => {
if (!auto) return;
const interval = setInterval(_ => {
next();
}, autoInterval);
return _ => clearInterval(interval);
},
[auto, current]
);
Conditionally executing update to state — it does not require useEffect dependencies
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, autoInterval);
return _ => clearInterval(interval);
});
Both solutions work if we replace setInterval by setTimeout
You could use useTimeout hook that returns true after specified number of milliseconds.
https://github.com/streamich/react-use/blob/master/docs/useTimeout.md

Resources