React useEffect with useState and setInterval - reactjs

using the following code to rotate an array of object through a component DOM. The issue is the state never updates and I can't workout why..?
import React, { useState, useEffect } from 'react'
const PremiumUpgrade = (props) => {
const [benefitsActive, setBenefitsActive] = useState(0)
// Benefits Details
const benefits = [
{
title: 'Did they read your message?',
content: 'Get more Control. Find out which users have read your messages!',
color: '#ECBC0D'
},
{
title: 'See who’s checking you out',
content: 'Find your admirers. See who is viewing your profile and when they are viewing you',
color: '#47AF4A'
}
]
// Rotate Benefit Details
useEffect(() => {
setInterval(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
}, [])
the output I get looks like the following image. I can see the useState 'setBenefitsActive' is being called but 'benefitsActive' is never updated.

You pass no dependencies to useEffect meaning it will only ever run once, as a result the parameter for setInterval will only ever receive the initial value of benefitsActive (which in this case is 0).
You can modify the existing state by using a function rather than just setting the value i.e.
setBenefitsActive(v => v + 1);

Some code for your benefit!
In your useEffect as #James suggested, add a dependency to the variable that's being updated. Also don't forget to clean up your interval to avoid memory leaks!
// Rotate Benefit Details
useEffect(() => {
let rotationInterval = setInterval(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
//Clean up can be done like this
return () => {
clearInterval(rotationInterval);
}
}, [benefitsActive]) // Add dependencies here
Working Sandbox : https://codesandbox.io/s/react-hooks-interval-demo-p1f2n
EDIT
As pointed out by James this can be better achieved by setTimeout with a much cleaner implementation.
// Rotate Benefit Details
useEffect(() => {
let rotationInterval = setTimeout(() => {
console.log(benefits.length)
console.log(benefitsActive)
if (benefitsActive >= benefits.length) {
console.log('................................. reset')
setBenefitsActive(0)
} else {
console.log('................................. increment')
setBenefitsActive(benefitsActive + 1)
}
}, 3000)
}, [benefitsActive]) // Add dependencies here
Here, a sort of interval is created automatically due to the useEffect being called after each setTimeout, creating a closed loop.
If you still want to use interval though the cleanup is mandatory to avoid memory leaks.

When you pass a function to setInterval, you create a closure, which remembers initial value of benefitsActive. One way to get around this is to use a ref:
const benefitsActive = useRef(0);
// Rotate Benefit Details
useEffect(() => {
const id = setInterval(() => {
console.log(benefits.length);
console.log(benefitsActive.current);
if (benefitsActive.current >= benefits.length) {
console.log("................................. reset");
benefitsActive.current = 0;
} else {
console.log("................................. increment");
benefitsActive.current += 1;
}
}, 3000);
return () => clearInterval(id);
}, []);
Demo: https://codesandbox.io/s/delicate-surf-qghl6

I've had the same problem and found a perfect solution on
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
using an own hook
import { useRef, useEffect } from "react";
export function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
using it like
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);

Related

React useState is not updating when set method is called in an interval

I have a websocket server that sends an object containing some hashes every 15 seconds. When the client receives a hash, I want to check with my current hash. If they differ, I want to make a call to an API to fetch new data.
The socket is working and sending the hash correctly. If the data updates on the server I get a different hash. My problem is that the hash variable I use to store the current hash is not updated correctly.
I have disabled the socket listening in my component, just to make sure that that is not the problem. Instead I have added a setInterval to mimik the socket update.
This is my code (socked code disabled but left as a comment):
import { useCallback, useEffect, useState } from "react";
import { useAuth, useSocket } from "../utils/hooks";
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState<QuestionListModel[]>([]);
const { user } = useAuth();
const { socket } = useSocket();
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response: ApiResponse | boolean = await getQuestionLists(user?.token);
if (typeof response !== "boolean" && response.data) {
setQuestionLists(response.data);
}
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = useCallback((newHash: Hash) => {
console.log("called update");
let shouldUpdate = false;
let originalHash = { ...hash };
let updatedHash = { ...newHash };
console.log("new: ", newHash);
console.log("stored: ", originalHash);
if (hash.questionList !== newHash.questionList) {
console.log("was not equal");
updatedHash = { ...updatedHash, questionList: newHash.questionList}
shouldUpdate = true;
}
if (shouldUpdate) {
console.log("trying to set new hash: ", updatedHash);
setHash(updatedHash);
fetchQuestionLists();
}
}, [hash]);
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, 15000)
}, []);
return (
<>
... Things here later ...
</>
);
};
export default Admin;
After the initial render, and waiting two interval cycles, this is what I see in the console:
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
You can see that stored is empty. That leads me to believe that setHash(updatedHash); never runs for some reason. Why is that?
Having hacked about with this in codepen here: https://codesandbox.io/s/as-prop-base-forked-l3ncvo?file=/src/Application.tsx
This seems to me to be a closure issue as opposed to a React issue. If you have a look in the dev tools, you'll see the state of the component is doing what you're expecting it to. The issue is that the console log is not.
useEffect is only ever going to use an old version of update, so the console won't log what you're expecting. If you add update to the dependency array (and add a clean up so we don't end up with tonnes of intervals) you'll get what you're looking for. Can be seen in the linked codepen.
I think the issue in on this line :
socket?.on('aHash', (hash) => update(hash));
maybe when you register a listener, it keeps the first value of update only,
can you please share useSocket?
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
Include setHash in your dependency list et voilà
EDIT: Or well, you should include these dependencies in all your useCallback/useEffect hooks since the reference will be lost whenever the component updates. You always have to include all dependencies in the dependency list not to get unpredictable behavior.
use setState(prevValue => {}) to get the the preferred effect. Also, if you running in a Strict mode this will fire the setState twice.
Here is how the code should look like:
import { useCallback, useEffect, useState } from "react";
import { faker } from '#faker-js/faker';
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState([]);
const [ hash, setHash ] = useState({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response = {data: {hash: 'asdf-1234'}}
setQuestionLists(response.data);
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = (newHash) => {
console.log("called update");
setHash(oldHash => {
console.log('old hash: ', oldHash);
console.log('new hash', newHash);
if (JSON.stringify(oldHash) !== JSON.stringify(newHash)) {
return newHash
}
})
};
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: faker.random.numeric(36)});
}, 15000)
}, []);
return (
<>
<h2>Hash</h2>
{JSON.stringify(hash)}
</>
);
};
export default Admin;
In both cases (socket & interval) the issue is that you need to re-define the callback functions with the new context of the variables in the scope, whenever something changes. In this case you will probably need to put "update" (and whatever other variable you need to "watch") inside the dependancy array of the useEffect.
Ive had a similar issues. Here is how I ended up defining socket callback that updates correctly. Notice that I added the save function (just a function that saves the state into the useState). Also, you need to return a clean up function to turn the socket callback off when the component unmounts. This way every time anything changes in the dependancy array, the hook re-runs and recreates that callback with the new information.
React.useEffect(() => {
socketRef?.current?.on(
'private_message_sent_to_client',
(data: IMessageResult) => {
savePrivateMessages(data);
}
);
return () => {
socketRef?.current?.off('private_message_sent_to_client');
};
}, [meta, selectedChatId, savePrivateMessages]);
And here is an example for you
React.useEffect(() => {
socket?.on('aHash', (hash) => update(hash));
return () => {
socket?.off('aHash')
};
}, [update, hash]);

How to use debounce with React

I've seen quite a few similar questions here, but none of the suggested solutions seemed to be working for me. Here's my problem (please notice that I'm very new to React, just learning it):
I have the following code in my App.js:
function App() {
const [movieSearchString, setMovieSearchString] = useState('')
const providerValue = {
moviesList: moviesList,
movieSearchString: movieSearchString,
}
const searchMovieHandler = () => {
console.log('movie handler called')
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = movieSearchString
}
debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000)
}
const movieInputChangeHandler = string => {
console.log('onMovieInputChange', string);
setMovieSearchString(string)
searchMovieHandler()
}
return (
<MoviesContext.Provider value={providerValue}>
<div className="App d-flex flex-column px-4 py-2">
<SearchBar
onMovieInputChange={movieInputChangeHandler}
/>
...Rest of the content
</div>
</MoviesContext.Provider>
);
}
In this situation all the console.logs get called EXCEPT the one that should be debounced (I tried both lodash debounce and my own, none worked; currently kept the lodash version).
So I tried to comment out that debounce call and tried to use it like that:
useEffect(() => {
console.log('use effect 1')
debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000)
}, [movieSearchString])
I'm getting the use effect 1 log when the movieSearchString changes, but not the debounced one.
So I tried to do this:
const debounced = useRef(debounce(() => {
console.log('deb: ' + movieSearchString)
}, 1000))
useEffect(() => debounced.current(movieSearchString), [movieSearchString])
In this case I'm getting the console log deb: after a second, but no movieSearchString is printed.
I don't know what else I can do here... Eventually what I want is when a user enters something in the text field, I want to send an API call with the entered string. I don't want to do it on every key stroke, of course, thus need the debounce. Any help, please?
Try to debounce the state value, not to debounce effect itself. It's easier to understand.
For example, you can use custom hook useDebounce in your project:
useDebounce.js
// borrowed from https://usehooks.com/useDebounce/
function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
App.js
const [movieSearchString, setMovieSearchString] = useState('')
const debouncedMovieSearchString = useDebounce(movieSearchString, 300);
const movieInputChangeHandler = string => {
setMovieSearchString(string)
}
useEffect(() => {
console.log('see useDebounceValue in action', debouncedMovieSearchString);
}, [debouncedMovieSearchString]);
useEffect(() => {
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = debouncedMovieSearchString
}
callApi(params);
}, [debouncedMovieSearchString]);
Refer to this article: https://usehooks.com/useDebounce/
You need to wrap the function you want to debounce wit the debounce() function like this:
const searchMovieHandler = debounce(() => {
console.log('movie handler called')
const params = {
apikey: 'somekey'
}
if (movieSearchString.length > 2) {
params.search = movieSearchString
}
}, 1000);

Why using useRef in this react example? just for concept demostration?

Just wonder what purpose the useRef serve here in example: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
}
I tried and can achieve the same without useRef as below:
function Timer() {
const interval = null;
useEffect(() => {
const id = setInterval(() => {
// ...
});
interval = id;
return () => {
clearInterval(interval);
};
});
// ...
function handleCancelClick() {
clearInterval(interval);
}
// ...
}
So the saying "but it’s useful if we want to clear the interval from an event handler" from the react doc and this answer: Is useRef Hook a must to set and clear intervals in React?, just mean almost nothing at all.
It's fine only if you don't want stopping timer in handleCancelClick and keep all logic inside single useEffect(which would be really rare case).
See, if you get any re-render(because of any useState entry changed or props changed) between running timer and handleCancelClick you will get that variable const interval = null; and nothing will happen on click(clearTimeout(null); does nothing).
Don't see how that can be handled without preserving data between renders.

Skip first useEffect when there are multiple useEffects

To restrict useEffect from running on the first render we can do:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
According to example here: https://stackoverflow.com/a/53351556/3102993
But what if my component has multiple useEffects, each of which handle a different useState change? I've tried using the isFirstRun.current logic in the other useEffect but since one returns, the other one still runs on the initial render.
Some context:
const Comp = () => {
const [ amount, setAmount ] = useState(props.Item ? Item.Val : 0);
const [ type, setType ] = useState(props.Item ? Item.Type : "Type1");
useEffect(() => {
props.OnAmountChange(amount);
}, [amount]);
useEffect(() => {
props.OnTypeChange(type);
}, [type]);
return {
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
}
}
The reason I've used separate useEffects for each is because if I do the following, it doesn't update the amount.
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
As far as I understand, you need to control the execution of useEffect logic on the first mount and consecutive rerenders. You want to skip the first useEffect. Effects run after the render of the components.
So if you are using this solution:
const isFirstRun = useRef(true);
useEffect (() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
console.log("Effect was run");
});
useEffect (() => {
// second useEffect
if(!isFirstRun) {
console.log("Effect was run");
}
});
So in this case, once isFirstRun ref is set to false, for all the consecutive effects the value of isFirstRun becomes false and hence all will run.
What you can do is, use something like a useMount custom Hook which can tell you whether it is the first render or a consecutive rerender. Here is the example code:
const {useState} = React
function useMounted() {
const [isMounted, setIsMounted] = useState(false)
React.useEffect(() => {
setIsMounted(true)
}, [])
return isMounted
}
function App() {
const [valueFirst, setValueFirst] = useState(0)
const [valueSecond, setValueSecond] = useState(0)
const isMounted = useMounted()
//1st effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueFirst ran")
}
}, [valueFirst])
//2nd effect which should run whenever valueFirst change except
//first time
React.useEffect(() => {
if (isMounted) {
console.log("valueSecond ran")
}
}, [valueSecond])
return ( <
div >
<
span > {
valueFirst
} < /span> <
button onClick = {
() => {
setValueFirst((c) => c + 1)
}
} >
Trigger valueFirstEffect < /button> <
span > {
valueSecond
} < /span> <
button onClick = {
() => {
setValueSecond((c) => c + 1)
}
} >
Trigger valueSecondEffect < /button>
<
/div>
)
}
ReactDOM.render( < App / > , document.getElementById("root"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I hope it helps !!
You can use a single useEffect to do both effects in, you just implemented the logic incorrectly.
Your original attempt:
useEffect(() => {
if (amount) {
props.OnAmountChange(amount);
} else if (type) {
props.OnTypeChange(type)
}
}, [amount, type]);
The issue here is the if/elseif, treat these as independent effects instead:
useEffect(() => {
if (amount !== 0) props.onAmountChange(amount);
if (type !== "Type1") props.onTypeChange(type);
}, [amount, type])
In this method if the value is different than the original value, it will call the on change. This has a bug however in that if the user ever switches the value back to the default it won't work. So I would suggest implementing the entire bit of code like this instead:
const Comp = () => {
const [ amount, setAmount ] = useState(null);
const [ type, setType ] = useState(null);
useEffect(() => {
if (amount !== null) {
props.onAmountChange(amount);
} else {
props.onAmountChange(0);
}
}, [amount]);
useEffect(() => {
if (type !== null) {
props.onTypeChange(type);
} else {
props.onTypeChange("Type1");
}
}, [type]);
return (
<>
// Radio button group for selecting Type
// Input field for setting Amount
</>
)
}
By using null as the initial state, you can delay calling the props methods until the user sets a value in the Radio that changes the states.
If you are using multiple useEffects that check for isFirstRun, make sure only the last one (on bottom) is setting isFirstRun to false. React goes through useEffects in order!
creds to #Dror Bar comment from react-hooks: skip first run in useEffect

Is useRef Hook a must to set and clear intervals in React?

I'm currently understanding the useRef hook and its usage. Accessing the DOM is a pretty straight forward use case which I understood. The second use case is that a ref behaves like an instance field in class components. And the react docs provide an example of setting and clearing a time interval from a click handler. I want to know, if cancelling the time interval from a click handler is not required, can we set and clear intervals with local variables declared within useEffect like below? Or is using a ref as mentioned in the docs always the approach to be taken?
useEffect(() => {
const id = setInterval(() => {
// ...
});
return () => {
clearInterval(id);
};
})
As stated at the docs you shared;
If we just wanted to set an interval, we wouldn’t need the ref (id could be local to the effect).
useEffect(() => {
const id = setInterval(() => {
setCounter(prev => prev + 1);
}, 1000);
return () => {
clearInterval(id);
};
});
but it’s useful if we want to clear the interval from an event handler:
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
I think the example is just for demonstrating how useRef works, though I personal cannot find many use case for useRef except in <input ref={inputEl} /> where inputEl is defined with useRef. For if you want to define something like an component instance variable, why not use useState or useMemo? I want to learn that too actually (Why using useRef in this react example? just for concept demostration?)
As for the react doc example https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
}
I tried and can achieve the same without useRef as below:
function Timer() {
const interval = null;
useEffect(() => {
const id = setInterval(() => {
// ...
});
interval = id;
return () => {
clearInterval(interval);
};
});
// ...
function handleCancelClick() {
clearInterval(interval);
}
// ...
}

Resources