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.
Related
I took code based off this page and adjusted it. I want to time the amount of milliseconds the user is on the component so I want to log the counter value when the component unmounts aka the return statement of useffect/componentWillUnmount().
const [milliseconds, setMilliseconds] = useState(0);
const isActive = useState(true);
const logger = new logger(stuff);
useEffect(() => {
initializeIcons(undefined, { disableWarnings: true });
});
useEffect(() => {
return () => {
console.log("empty useffect milliseconds:", milliseconds);
logger(milliseconds);
clearInterval(milliseconds)
};
}, []);
useEffect(() => {
let interval: NodeJS.Timeout = setInterval(() => {
}, 0);
interval = setInterval(() => {
setMilliseconds(milliseconds => milliseconds + 1000);
}, 1000);
console.log("interval:", interval);
console.log("interval milliseconds:", milliseconds);
}, [ milliseconds]);
I see the millisecond printout fine in the "interval milliseconds" console statement but the "empty useffect milliseconds:" always prints out 0. What am I doing wrong?
You can remember a mount timestamp and then calculate the difference.
useEffect(() => {
const mountedAt = Date.now();
return () => {
const mountedMilliseconds = Date.now() - mountedAt;
console.log(mountedMilliseconds);
};
}, []);
Side note 1: use an empty array as deps if you want to run function on mount only. If you do not pass [] deps, your initializeIcons effect will run with each re-render. Do it like this:
useEffect(() => {
initializeIcons(undefined, { disableWarnings: true });
}, []);
Side note 2: first interval you create creates a memory leak, because it does nothing, and is never cleared.
Another problem you have is milliseconds dependency in useEffect, which registers new intervals after each milliseconds state change.
I've been trying to solve this but no matter what solution I do, it is still stuck in a infinite loop.
Here is the code
const [carr, setCarr] = useState({})
useEffect(() => {
sortedRosterCollection.once('value', (snap) => {
snap.forEach((doc) =>{
if (doc.key==="Carr Intermediate"){
var school = doc.key;
var mentorList = doc.val();
var schoolMentor = {school:school, mentors: mentorList};
setCarr(schoolMentor)
console.log(carr)
}
});
});
},[carr]);
No matter what I do "console.log(carr)" is fired endlessly.
If you want to inspect the value of carr whenever it's changed, put it into another use effect:
const [carr, setCarr] = useState({})
useEffect(() => {
sortedRosterCollection.once('value', (snap) => {
snap.forEach((doc) => {
if (doc.key === "Carr Intermediate") {
var school = doc.key;
var mentorList = doc.val();
var schoolMentor = {
school: school,
mentors: mentorList
};
setCarr(schoolMentor)
}
});
});
}, []);
useEffect(() => {
console.log(carr)
}, [carr])
You don't forget cleanup function in useEffect hook like this:
useEffect(() => {
effect
return () => {
cleanup
}
}, [input])
I used to make this problem like you because useEffect can compare two object.
You can make the reference to the link: https://medium.com/javascript-in-plain-english/comparing-objects-in-javascript-ce2dc1f3de7f. And I check changed by using below code:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
I'm following this article by Dan Abramov:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
In the article, Dan makes a custom useInterval hook, to create a dynamic setInterval.
The hook looks like this:
export default function useInterval(callback, delay) {
//this useInterval function will be called whenever the parent component renders.
// on render, savedCallback.current gets set to whatever the callback is, if the callback
// has changed
const savedCallback = useRef();
console.log("called")
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
/**
* Likewise, the set interval is set off,
* and if delay is
*/
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => {
console.log("clearEed!")
clearInterval(id);
}
}
}, [delay]);
}
There's a part I don't understand though, which is here:
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => {
console.log("clearEed!")
clearInterval(id);
}
}
}, [delay]);
I understand that this useEffect is called if the delay is changed. The callback is assigned to tick, then if the delay isn't null, id is set to the SetInterval, with tick and the delay as parameters. This all makes sense. But what happens next is strange to me. I know useEffect can take a return statement for when the component unmounts, but why are we clearing the interval we set just before? I'd really appreciate it if someone could talk me through this.
In particular, I'd really like help understanding these lines:
if (delay !== null) {
let id = setInterval(tick, delay);
return () => {
console.log("clearEed!")
clearInterval(id);
}
}
I'm using it like this:
function TimerWithHooks() {
let [count, setCount] = useState(0);
let [delay, setDelay] = useState(1000);
useInterval(() => {
setCount(count + 1);
}, delay)
const handleDelayChange = evt => {
setDelay(Number(evt.target.value))
}
return (
<>
<h1>{count}</h1>
<input value={delay} onChange={handleDelayChange} />
</>
);
}
export default TimerWithHooks;
Effects with Cleanup
When exactly does React clean up an effect?
React performs the cleanup when the component unmounts. However, as we
learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. We’ll discuss why this helps avoid bugs
and how to opt out of this behavior in case it creates performance
issues later below.
This means that every time the delay changes, the Effect will cleanup previously effects, thus it will clear the timer every time we change the delay and NOT when the component unmounts. This way, we can adjust the timer dynamically without having to worry about clearing the timers.
I guess Dan clear timer when the component will unmount, but I think beater make this after the function executed. something lick this:
useEffect(() => {
if (delay !== null) {
let timerId = setInterval(
() => {
savedCallback.current();
clearInterval(timerId);
},
delay
);
}
}, [delay]);
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);
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);
}
// ...
}