I have a state that tracks the window width:
const [innerWidth, setInnerWidth] = useState(window.innerWidth)
In useEffect, I create a resize eventListener which sets the state to the new width:
useEffect(() => {
document.addEventListener('resize', () => {
setInnerWidth(window.innerWidth)
})
}, [])
Lastly, I have a function test that logs the innerWidth every 5 seconds, with an interval started in useEffect
function test() {
console.log(innerWidth)
}
useEffect(() => {
setInterval(test, 5000)
}, [])
Unfortunately, despite any resize that happen, the test() function keeps on logging the original innerWidth value.
How can I tell react to reload the test function as well?
EDIT:
The perpetual log of the innerWidth was just a simplification of my actual use case. Actually, the timer is shifting an element on the x-axis, and I need to know when it exceeds the width to stop the execution and start again.
Creating and invalidating a loop every time the window changes, like in several answers you've given, temporarily stops the shifting of my element, as the loop gets invalidated. I would like to avoid this.
The useEffect created a closure around the original values, so that's all it ever logs. You'd need the effect to update any time the value changes, by adding it to the dependency array:
useEffect(() => {
setInterval(test, 5000)
}, [innerWidth])
This would of course create a new interval on every state change. So the useEffect should return a function which cancels the interval:
useEffect(() => {
const x = setInterval(test, 5000);
return () => clearInterval(x);
}, [innerWidth])
That way there's only one interval running at any given time.
Though this begs the question... Why? If the goal is to log the value of innerWidth to observe its changes, then why re-log the same value every 5 seconds indefinitely? Skip the test function and the interval entirely and just log the value any time it changes:
useEffect(() => {
console.log(innerWidth);
}, [innerWidth])
Can you change the test function to an anonymous function?
const test = () => {
console.log(innerWidth);
};
Change you useEffect:
useEffect(() => {
window.addEventListener('resize', () => {
setInnerWidth(window.innerWidth);
});
}, [setInnerWidth]);
The solution was wrapping the innerWidth into an object, so that it is passed by reference and it 'updates' in the test function.
const innerWidthWrapper = {width: window.innerWidth}
useEffect(() => {
innerWidthWrapper.width = window.innerWidth
})
}, [])
Edit: Your issue using the interval function is explained in this answer
This code works for me by logging the state variable using the effect hook:
const [innerWidth, setInnerWidth] = useState(window.innerWidth);
useEffect(() => {
const updateWidth = () => {
setInnerWidth(window.innerWidth);
};
window.addEventListener("resize", updateWidth);
}, []);
useEffect(() => {
console.log(innerWidth);
}, [innerWidth]);
I added the eventlistener using the window object.
Sandbox
Related
I have two onclick functions that performing start and stop functions using socket when i click start it start requesting images from server(i am using intervals) when i click stop the server is stopped but the request keep coming to the server. the intervals seems not stopped after clicking kindly review my code
Code:
var interval;
function onclick() {
interval = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350)
}
function onclicks() {
clearInterval(interval);
setImage(blanked);
}
I have tried to add clearInterval function but it seems not working
Variables declared at the top level of a component are redeclared on each render (here every time you call setImage()) so interval is undefined by the time onclicks() is called.
In order to maintain the value through renders you need to either use state (useState) or a ref (useRef) which won't lose it's value. In this case, since you don't want renders attached to the value of interval a ref is more appropriate. useRef() returns an object with a current property in which your value will be stored.
const interval = React.useRef(null);
function onclick() {
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350);
}
function onclicks() {
clearInterval(interval.current);
interval.current = null; // reset the ref
setImage(blanked);
}
You probably also want to avoid setting multiple intervals. You may already be doing so, but if not you can check if interval is null or not before setting a new one.
const interval = React.useRef(null);
function onclick() {
if (interval.current === null) { // only set new interval if interval ref is null
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350);
}
}
For me, the easiest way to solve this behavior was set the interval as a Ref:
import { useRef } from 'react';
// Initialize the interval ref
const interval = useRef();
// Create the interval
function onclick() {
interval.current = setInterval(() => {
socket.emit("get_image", "Click");
socket.on('send_image', (message) => {
setImage(message.image);
});
}, 350)
}
// Remove the interval
function onclicks() {
clearInterval(interval.current);
setImage(blanked);
}
Using this hook, the component will not re-render when you change the value remove/change the interval.
Attempted to translate an example code from class to functional component and faced the problem.
the target file is in components/Wheel/index.js
Key function that causes problem
const selectItem = () => {
if (selectedItem === null) {
const selectedItem = Math.floor(Math.random() * items.length);
console.log(selectedItem);
setSelectedItem(selectedItem);
} else {
setSelectedItem(null);
let t= setTimeout(() => {
selectItem()
}, 500);
clearTimeout(t);
}
};
First time is normal,
from second time onward,
2 clicks are needed for the wheel to spin.
I had to add clearTimeout() or infinite loop is resulted, but the same does not happen in the original.
Original working example in class
My version in functional component.
MyVersion
Thank you.
What an excellent nuance of hooks you've discovered. When you call selectItem in the timeout, the value of selectedItem that is captured in lexical scope is the last value (not null).
There's two answers, a simple answer and a better working answer.
The simple answer is you can accomplish it be simply separating the functions: https://codesandbox.io/s/spinning-wheel-game-forked-cecpi
It looks like this:
const doSelect = () => {
setSelectedItem(Math.floor(Math.random() * items.length));
};
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
setTimeout(doSelect, 500);
}
};
Now, read on if you dare.
The complicated answer fixes the solution for the problem if items.length may change in between the time a timer is set up and it is fired:
https://codesandbox.io/s/spinning-wheel-game-forked-wmeku
Rerendering (i.e. setting state) in a timeout causes complexity - if the component re-rendered in between the timeout, then your callback could've captured "stale" props/state. So there's a lot going on here. I'll try and describe it as best I can:
const [selectedItem, setSelectedItem] = useState(null);
// we're going to use a ref to store our timer
const timer = useRef();
const { items } = props;
// this is just the callback that performs a random select
// you can see it is dependent on items.length from props
const doSelect = useCallback(() => {
setSelectedItem(Math.floor(Math.random() * items.length));
}, [items.length]);
// this is the callback to setup a timeout that we do
// after the user has clicked a "second" time.
// it is dependent on doSelect
const doTimeout = useCallback(() => {
timer.current = setTimeout(() => {
doSelect();
timer.current = null;
}, 500);
}, [doSelect]);
// Here's the tricky thing: if items.length changes in between
// the time we rerender and our timer fires, then the timer callback will have
// captured a stale value for items.length.
// The way we fix this is by using this effect.
// If items.length changes and there is a timer in progress we need to:
// 1. clear it
// 2. run it again
//
// In a perfect world we'd be capturing the amount of time remaining in the
// timer and fire it exactly (which requires another ref)
// feel free to try and implement that!
useEffect(() => {
if (!timer.current) return;
clearTimeout(timer.current);
doTimeout();
// it's safe to ignore this warning because
// we know exactly what the dependencies are here
}, [items.length, doTimeout]);
const selectItem = () => {
if (selectedItem === null) {
doSelect();
} else {
setSelectedItem(null);
doTimeout();
}
};
In my react native project I need to create a blinking (like warning) text. For this I use setInterval inside useffect() where I switch the text visibility by using setShowText((showText) => !showText). Now I need to to stop the setInterval when another state variable stopTriger changes to true. (I have a sperate method to set setStopTriger). Now the situation is like console.log('timer is running infinitely'). How can I make it to work? I use the following code, but not sure where to keep stopTriger condition:
const [stopTriger, setStopTriger] = useState(false)
const [showText, setShowText] = useState(true);
useEffect(() => {
// Change the state every second
const interval = setInterval(() => {
setShowText((showText) => !showText);
console.log('timer is running infinitely ')
// need to stop timer when stopTriger == true , how do I do it ?
}, 1000);
return () => clearInterval(interval);
}, []);
You can put intervalID in a state, and use another useEffect with stoTrigger as a dependency, when that variable is true, you can clear the interval:
Update
const [interval, setStateInterval] = useState(0);
useEffect(() => {
// Change the state every second
const interval_ = setInterval(() => {
/* set value of interval state */
setStateInterval(interval_);
setShowText(showText => !showText);
console.log('timer is running infinitely ');
// need to stop timer when stopTriger == true , how do I do it ?
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (stopTrigger) clearInterval(interval);
}, [stopTriger]);
Also, clearInterval() will automatically take care if the interval is currently running before clearing it, so you don't need to worry
stopTriger is initially false, and you aren't modifying it.
useEffect(() => {
playLoop();
}, [state.playStatus]);
const playLoop = () => {
if (state.playStatus) {
setTimeout(() => {
console.log("Playing");
playLoop();
}, 2000);
} else {
console.log("Stopped");
return;
}
};
Output:
Stopped
// State Changed to true
Playing
Playing
Playing
Playing
// State Changed to false
Stopped
Playing // This is the problem, even the state is false this still goes on execute the Truthy stalemate
Playing
Playing
I am working on react-native and I want the recursion to stop when the state value becomes false.
Is there any other way I can implement this code I just want to repeatedly execute a function while the state value is true.
Thank you
Rather than having a playStatus boolean, I'd save the interval ID. That way, instead of setting playStatus to false, call clearInterval. Similarly, instead of setting playStatus to true, call setInterval.
// Can't easily use useState here, because you want
// to be able to call clearInterval on the current interval's ID on unmount
// (and not on re-render) (interval ID can't be in an old state closure)
const intervalIdRef = useRef(-1);
const startLoop = () => {
// make sure this is not called while the prior interval is running
// or first call clearInterval(intervalIdRef.current)
intervalIdRef.current = setInterval(
() => { console.log('Playing'); },
2000
);
};
const stopLoop = () => {
clearInterval(intervalIdRef.current);
};
// When component unmounts, clean up the interval:
useEffect(() => stopLoop, []);
The first thing you should do is make sure to clear the timeout when the state changes to stopped or otherwise check the state within the timeout callback function.
But the problem does not seem to be with the setTimeout code only by itself, but rather that this playLoop is also being called too many times. You should add a console.log with a timestamp right at the start of your playLoop to confirm or disprove this. And to find out where it is called from, you could use console.trace.
const playLoop = () => {
console.log(new Date(), ': playLoop called')
console.trace(); // optional
if (state.playSt....
I'm listening to a scroll event in a function for a pagination feature. The scroll event is called inside a useEffect hook. When the scroll event is fired, I want to make a HTTP call with a value already set in state, and update that value if the HTTP request is successful.
However, whenever the event fires, it only uses the initial value of the item in state, even though the value has been updated, and I can see the updated value outside of the function.
How do I get the updated value inside the onScroll callback?
Below is a code snippet
const [test, setTest] = useState(0);
// The below commented out code works because it's not inside useEffect
// window.addEventListener('scroll', () => requestAnimationFrame(() => {
// console.log(test);
// setTest(test + 1);
// }));
useEffect(() => {
window.addEventListener('scroll', () => requestAnimationFrame(() => {
console.log(test); // this always returns 0
setTest(test + 1);
}));
}, [])
console.log(test); // this returns the updated/correct value.
By providing an empty array as the second argument to the effect, you've told react to never recreate this effect. So it happens just once, and the variables it has in its closure are never going to change.
Option 1:
You can get the effect to rerun by either removing the dependency array, or populating it with the variables you care about. Since this will result in the effect running multiple times you will also need to provide a cleanup function for tearing down the previous effect.
useEffect(() => {
const callback = () => requestAnimationFrame(() => {
setTest(test + 1);
})
window.addEventListener('scroll', callback);
return () => window.removeEventListener('scroll', callback);
}) // <------
Option 2:
Since the only thing you need to be refreshed is the latest value of state, you can use the callback version of setTest. React will pass you in the latest value, and you can compute the change based on that.
You will still want to have a teardown function so that the listener can be removed if this component unmounts:
useEffect(() => {
const callback = () => requestAnimationFrame(() => {
setTest(prev => prev + 1); // <------
})
window.addEventListener('scroll', callback);
return () => window.removeEventListener('scroll', callback);
}, [])