I need to create an event handler in React in a custom hook and I'd like the event handling function to be referentially equal across renders for ..reasons (yes I do see how I can do this w/o but I want to understand). Can I use useCallback as below to ensure the eventListener is referentially equal on every render by simply not mentioning variables it closes over in the dependencies (as below) or will that cause issues even if it's never passed to a child component so I don't need it to change value to trigger rerenders?
If not how can I achieve this? Does it matter if I can assume delay and fn don't change (except perhaps it's reference)?
export const useVisibleInterval = (fn, delay = 300000) => {
const interval_id = useRef(null)
const handleVisibilityChange = useCallback(() => {
if (document.hidden || interval_id.current) return
interval_id.current = setInterval(fn, delay)
fn()
}, [])
const clearInt = () => {
if (!interval_id.current) return
clearInterval(interval_id.current)
interval_id.current = null
}
const interval_run = () => {
if (document.hidden) {
document.addEventListener("visibilitychange", handleVisibilityChange)
clearInt()
} else fn()
}
const teardown = () => {
document.removeEventListener("visibilitychange", handleVisibilityChange)
clearInt()
}
useEffect(() => {
interval_id.current = setInterval(interval_run, delay)
return teardown
}, [])
}
At a really high level, I'd like to understand what the dependencies for useCallback do. Are they just to force reference inequality so child components that depend on it rerender? Or does React memoize the result of function execution so failing to include dependencies would give stale results?
Based on pilchard's comment I've figured out the answer (and that I was being an idiot).
The issue is merely about what values are updated when the function gets called with new arguments. References don't need to be listed in the dependencies of a useCallback so the way I can do this is simply to replace fn and delay with references that take on those values, e.g.,
ref_fn = useRef(fn)
useEffect(()=> {ref_fn.current = fn}, [fn]}
And likewise for delay (or stuff them into same ref). But, it turns out that I didn't even need the useCallback and referential equality and the code above reflects my deep confusion at the time and should be ignored.
Related
I have a hook that adds a header tag in order to load a script, but in case two components are using it, the hook could fall into the case that the header is added twice.
There are several ways to avoid this. Two of them could be:
Use a provider, not a hook that several components could call. Actually I don't want this, I want a hook because at most two components will use it, and in some cases I don't even need it;
The hook calls a function called const addHeaderTag = () => {...}. I could add a property to it and check if the function is called just once, otherwise return silently. It could be safe because the function is defined by me and I control the function object, plus javascript is monothread and concurrency is out of scope;
I could add an id to the header so as to check if it's on the DOM already. I'd avoid it in order to not access the DOM too much
Do you see other better ways? Do you see any problem to the solutions I had in mind?
A solution for this would be using a variable outside of your custom hook to check whether or not your hook is already called
import { useEffect } from "react";
let isCalled = false;
export const useOnce = () => {
useEffect(() => {
if (!isCalled) {
// do this only once, call your function here
console.log("hey there");
isCalled = true;
}
}, []);
return isCalled;
};
The reason this works is because when you import the same module multiple times, the code in that module is still only evaluated once.
That means isCalled in this case is only initialized once, so we can depend on it to check/set the value accordingly for the entire app.
Live example
I'm trying to use a callback method inside useEffect. The function of the callback is to 'do something' with the component's props but I can't get access to the props inside of the callback
For a little bit more context I'm using the animation library called gsap and I'm doing some scroll animations and I'm trying to use the callback method 'onEnter'. This method attaches an event listener to the scroll and runs some code when the element comes into view, pretty straight forward.
I'm integration this with a react app and I'm having some problems getting the callback function to get the values of the props, I can receive them inside useEffect but not the callback.
Here is my code:
const Services = ({loaded, scroller}) => {
const [curSlide, setCurSlide] = useState(0);
// Animate in text
useEffect(() => {
if (loaded) {
// **** Scroll Animation and triggers for each slide ****
sliders.forEach((slider, i) => {
gsap.to(slider.title.current, {
scrollTrigger: {
//** HERE IS THE CALLBACK **//
onEnter: () => {
if (i === 0) return
setCurSlide(i)
console.log(scroller) // THIS RETURNS NULL WHEN SHOULD HAVE A VALUE
}
},
});
})
}
}, [loaded]) // Loaded only changes once
return (
<div>MAGICALLY WORK NOW?</div>
);
}
The prop I'm specifically referring to is "scroller"
I'm hoping it's something stupid I'm doing wrong - If I remove the usEffect I get access to the prop but I can't do that because it's causing a render loop, the same thing happens if I add curSlide to the useEffect Dependency Array. I don't understand why I get access to the prop after the component re-renders and not initially because the component should have access to it right away
I'm aware that we can use useEffect to set callbacks to state changes involving specific variables, but when I was refactoring a class component to one that uses hooks I encountered a problem, I set the state of a variable in three different functions (using setState), and each time I call a different callback for these setStates.
If I declare a useEffect like this:
useEffect(() => {
callback1();
callback2();
callback3();
}, [myVariable]);
Each time myVariable changes I will call those 3 callback functions, which is not my intent, since they interfere with each other.
To clarify things, this is what I was doing in the class component:
function a () {
...
this.setState({myVariable: x}, () => do something that involves myVariable);
};
function b () {
...
this.setState({myVariable: y}, () => do another thing that involves myVariable);
};
function c () {
...
this.setState({myVariable: z}, () => do another thing that involves myVariable);
};
How to proceed and create a specif callback for each setMyVariable call?
Thanks a lot!
Why not call the correct callback function directly in the function where you're calling setMyVariable()?
use multiple useEffects
useEffect(() => {
callback1();
}, [myVariable1]);
useEffect(() => {
callback2();
}, [myVariable2]);
I'm using socket.io along with react.js. I need to declare some event handlers for the socket, Also because of some modules I'm using, I can't use classic class components and therefore I'm using functions along with hooks. The problem is initializing the event handlers using the useEffect hook results in redeclaring the socket event handlers every time a new render happens, Thus, each event gets connected with a bunch of event handlers instead of only one, which results in multiple calls of the desired event handler when the event happens. There is way to make the useEffect run only once; Passing an empty array as a second argument to it (which kind of makes it like a constructor in class components). This prevents multiple calls on the event handler but, This also prevents the event handler from accessing the components latest state. Is there a way to achieve both?
var socket = io('http://localhost:10000');
const [var, setvar] = useState(0);
useEffect(() => {
socket.on('event', function(data) { //event handler function
console.log(var);
});
}, []);
For further explanation, The code above calls the event handler function only once per event but it always outputs the initial state of var which is 0. On the other hand, if the [] argument is removed, The handler will output the current value of var but it outputs it multiple times, Because for each render a new event handler is connected to the socket so the number of outputs depends on the number of renders that happened before that event.
Instead of the square brackets, you could pass in a 'dependency' for example [isInit]. The effect will only run if the value of isInit changes. You should declare isInit using useState and default it to false, i.e const [isInit, setIsInit] = useState(false). Then when you run the useEffect the first time call set setIsInit(true). Then in the useEffect check if (!isInit) ... this will control it nicely for you.
You can try combining what you have with a useRef, something like:
var socket = io('http://localhost:10000');
const [var, setvar] = useState(0);
const ref = useRef({current: var});
const alteredSetvar = useCallback((var) => {
setvar(var);
ref.current = var;
}, [setvar, ref]);
useEffect(() => {
socket.on('event', function(data) { //event handler function
console.log(ref.current);
});
}, []);
would work I think (call alteredSetvar instead of setvar.
useRef is typically used to hold a reference to a DOM node, but in reality it just holds an immutable reference, of which you can alter the inner value as much as you needed.
The function createHiveBackground returns an array of objects that I want to assign to a state. Later in my application I'm going to use setHive to change some values of the array. What is the difference between these?
const [hive, setHive] = React.useState(createHiveBackground);
const [hive, setHive] = React.useState(createHiveBackground());
const [hive, setHive] = React.useState(()=>createHiveBackground);
const [hive, setHive] = React.useState(()=>createHiveBackground());
If I use useState(createHiveBackground) it seems to work properly.
If I use useState(createHiveBackground()) each time I change a value with setHive the function is called again.
If I use useState(()=>createHiveBackground) TypeError: hive.map is not a function (seems like the function is not being executed).
If I use React.useState(()=>createHiveBackground()); it seems to work properly.
Can some clarify what is the difference between all these options and what is the best for my case?
The differences are between:
Value type: Function/Array (Function's return type).
Initial type: Normal/Lazy
// Lazy assign the return value of the function
// Both are equivalent as First-class functions (check the reference)
const [hive, setHive] = React.useState(createHiveBackground);
const [hive, setHive] = React.useState(() => createHiveBackground());
// Assign the return value from the function
const [hive, setHive] = React.useState(createHiveBackground());
// Lazy assign the function
const [hive, setHive] = React.useState(() => createHiveBackground);
References:
useState Hook
Lazy initial state
First Class Function
That's not even the question about react, that's simple js basics. I gonna explain on another subject, events function calls
document.addEventListener('change',(createHiveBackground));
document.addEventListener('change', (event)=>createHiveBackground(anotherParam));
The above both actions act the same, but the only difference how you coded createHiveBackground in first line your function handles by it own the incoming arguments as it would look like
function createHiveBackground(event, anotherArg){} because parent function addEventListener passes event as arguments in either cases as the first argument to the mentioned function.
But in the second case you coded the function which expects oneArgument only function createHiveBackground(anotherArg){} and you avoid conflicts between addEventListener and createHiveBackground so you use function in the middle
Another your line about difference with it:
const [hive, setHive] = React.useState(createHiveBackground());
Your createHiveBackground is written like this:
function createHiveBackground() {return data}
to provide a data that React.useState waiting for. This executes your func at once.
But this will not work at
document.addEventListener('change', createHiveBackground())
because it will be executed right in 'compilation' when no event have been fired. One exception it may return another function which will be called at event firing.
function createHiveBackground(){ return function(event, anotherParam){doSomeStuff on event}
The last example just passes a function body which may be called later
()=>createHiveBackground
it will not be invoked immediately
Coming back to your question hence React.useState expects some data - you may use any of these depending on createHiveBackground function implementation (the colleague above says:
//first two similar and used at lazy call
const [hive, setHive] = React.useState(createHiveBackground);
const [hive, setHive] = React.useState(()=>createHiveBackground());
//the next must return a data to be passed to the state
const [hive, setHive] = React.useState(createHiveBackground());
//nothing will happened because the function body will be passed but not executed
//function's body will be stored at the state
const [hive, setHive] = React.useState(()=>createHiveBackground);