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

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);
}
// ...
}

Related

Best practice interface for custom hooks with function arguments

I'm creating a custom hook to detect clicks inside/outside a given HTMLElement.
Since the hook accepts a function as an argument, it seems like either the input needs to be wrapped in a useCallback or stored inside the hook with useRef to prevent useEffect from triggering repeatedly.
Are both of the following approaches functionally the same?
Approach One (preferred)
// CALLER
useClickInsideOutside({
htmlElement: htmlRef.current,
onClickOutside: () => {
// Do something via anonymous function
},
});
// HOOK
const useClickInsideOutside = ({
htmlElement,
onClickOutside,
}) => {
const onClickOutsideRef = useRef(onClickOutside);
onClickOutsideRef.current = onClickOutside;
useEffect(() => {
function handleClick(event) {
if (htmlElement && !htmlElement.contains(event.target)) {
onClickOutsideRef.current && onClickOutsideRef.current();
}
}
document.addEventListener(MOUSE_DOWN, handleClick);
return () => { document.removeEventListener(MOUSE_DOWN, handleClick); };
}, [htmlElement]);
}
Approach Two
// CALLER
const onClickOutside = useCallback(() => {
// Do something via memoized callback
}, []);
useClickInsideOutside({
htmlElement: htmlRef.current,
onClickOutside,
});
// HOOK
const useClickInsideOutside = ({
htmlElement,
onClickOutside,
}) => {
useEffect(() => {
function handleClick(event) {
if (htmlElement && !htmlElement.contains(event.target)) {
onClickOutside();
}
}
document.addEventListener(MOUSE_DOWN, handleClick);
return () => { document.removeEventListener(MOUSE_DOWN, handleClick); };
}, [htmlElement, onClickOutside]);
}
Does the first one (which I prefer, because it seems to make the hook easier to use/rely on fewer assumptions) work as I imagine? Or might useEffect suffer from enclosing stale function references inside handleClick?
useCallback is the right approach for this. However, I think I'd design it in a way where I could abstract the memo-ing away from the consumer:
/**
* #param {MutableRefObject<HTMLElement>} ref
* #param {Function} onClickOutside
*/
const useClickInsideOutside = (ref, onClickOutside) => {
const { current: htmlElement } = ref;
const onClick = useCallback((e) => {
if (htmlElement && !htmlElement.contains(e.target)) {
onClickOutside();
}
}, [htmlElement, onClickOutside])
useEffect(() => {
document.addEventListener(MOUSE_DOWN, onClick);
return () => document.removeEventListener(MOUSE_DOWN, onClick);;
}, [onClick]);
}
And since I already touched on design, I'd try to make the design resemble similar API functions where 2 arguments are [in]directly related. I'd end up looking like this:
// Consumer component
const ref = useRef()
useClickOutside(ref, () => {
// do stuff in here
})

Custom Hooks SetInterval

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

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.

React useEffect with useState and setInterval

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

How remember that a React hooks component is unmounted, so can avoid state changes?

With old class based React.js code, I can do like this, to remember that a component has been unmounted:
componentWillUnmount: function() {
this.isGone = true;
},
loadUsers: function() {
Server.loadUsers(..., (response) => {
if (this.isGone) return;
...
});
}
How can one do the same thing, with hooks based components?
Here's a sample hooks function component, where I'm unsure about how to remember that it's been unmounted, so I can return before calling any setSomeState:
export const GroupMembers = React.createFactory(function(props) {
const [membersOrNull, setMembers] = React.useState(null);
let isGone = false; // ? (se [222] below
React.useEffect(() => {
Server.listGroupMembers(someGroupId, (response) => {
if (/* is unmounted ?? */) return;
setMembers(response.members);
});
return () => { /* how remember is unmounted?
would isGone = true; work? */ };
}, []);
...
return ..., Button({ title: "Remove all members", onClick: () => {
Server.removeAllMembers(someGroupId, () => {
if (/* is unmounted ?? */) return;
setMembers([]);
});
}});
I suppose I cannot use const [isGone, setGone] = useState(false) because I shouldn't try to access the state (read isGone) after has been unmounted. And if I [222] add a local let isGone = false inside the function, it seems to me that various callbacks created inside the function, will refer to different "instances" of this local variable, depending on in which different GroupMembers(..) invokation the different callbacks were created? or am I mistaken and this works? — Maybe I could create an outer wrapper function with a local let isGone = false, however, this adds another wrapping function and indentation :- /
Since you are using the isGone flag outside of the useEffect method too, you can make use of useRef to store variables like
export const GroupMembers = React.createFactory(function(props) {
const [membersOrNull, setMembers] = React.useState(null);
const isGone = useRef(false); // ? (se [222] below
React.useEffect(() => {
Server.listGroupMembers(someGroupId, (response) => {
if (isGone.current) return;
setMembers(response.members);
});
return () => {
isGone.current = true;
};
}, []);
...
return ..., Button({ title: "Remove all members", onClick: () => {
Server.removeAllMembers(someGroupId, () => {
if (isGone.current) return;
setMembers([]);
});
}});
PS: A better way to handle such things is to cancel the request when
you are leaving the page instead of waiting for the response only to
neglect it.
just use local variable accessed through closure
React.useEffect(() => {
let isActual = true;
Server.listGroupMembers(someGroupId, (response) => {
if (!isActual) return;
setMembers(response.members);
});
return () => {isActual = false;};
}, []);
in this case flag would be updated on unmounting only. but in general case(with some unempty dependencies for useEffect) it will use as well. So in case of sequential renderings you could be sure you never process older request.
PS most universal way is cancelling request when it's possible.

Resources