Is there a way to put requirements on hook dependencies? - reactjs

Currently, I am checking if the input useEffect() hook dependency is at least a certain length before calling loadSearchData() which is an async method that hits an API.
useEffect(() => {
if (input.length >= MIN_CHAR_INPUT) {
loadSearchData();
}
}, [input]);
Is there a way where I could move the input check to the input dependency param for useEffect()? Probably a case where I need to write a custom hook.

Is there a way where I could move the input check to the input dependency param for useEffect()? Probably a case where I need to write a custom hook.
I'd build it this way:
function useEffect2(effect, deps, isValid = true) {
const cleanup = React.useRef(null);
useEffect(() => {
if (isValid) {
if (typeof cleanup.current === "function") {
// schedule cancellation of previous request right after effect has been called
// using the Promise construct here so I don't have to deal with a throwing cancellation function
Promise.resolve().then(cleanup.current);
}
// in case effect() throws,
// don't want to call the old cancellation function twice
cleanup.current = null;
// get new cancel-function
cleanup.current = effect();
}
}, deps);
useEffect(() => () => {
// deal with cancellation on unmount
typeof cleanup.current === "function" && cleanup.current();
}, []);
}
useEffect2(loadSearchData, [input], input.length >= MIN_CHAR_INPUT);
I just want to clarify the cancel. This will give us access to the current useEffect() call in the stack and allow us to properly handle the call without any memory-leaks
From https://reactjs.org/docs/hooks-effect.html#recap
We’ve learned that useEffect lets us express different kinds of side effects after a component renders. Some effects might require cleanup so they return a function
Cleanup is probably a better name. I use it the most to "cancel" previous ajax-requests if they are still pending/prevent them to update the state. I've renamed the variable in the code.
What we're trying to emulate here is a useEffect that runs the effect conditionally. So when the condition is false, we don't want the effect to cleanup the previous call; as if the deps didn't change. Therefore we need to handle the cleanup function ourselves, and when/wether it should be invoked. That's
when (and only if) we call the effect function
on componentWillUnmount
That's what this ref is for. Since the reference is overwritten with every call to effect this shouldn't leak any memory.

Related

Why do I need cleanup in useEffect in react

Im new to react and Im not sure why do we need a cleanup function when dealing with EventListeners, Iam trying to set an event when resizing my window but i only have 1 event in (Elements -> Event Listeners) tab in chrome dev tools, even if I don't return a cleanup function in my hook
Heres my code:
useEffect(() => {
window.addEventListener("resize", function checksize() {
console.log("1");
});
});
First of all, you should absolutely avoid using that in your code, because on each rerender, it is going to add a new event listener to the window.
Secondly, to answer your question, you should have a cleanup effect to remove event listeners and other similar mechanisms to avoid memory leaks. If you don't clean them up, then you leave dangling eventlisteners taking up memory which is not a good idea, and may be picked up on within your other components as well. So the ideal way to handle this kind of code is
const logOne = () => console.log("1"); //put the function reference in a variable so that you can remove the event afterwards
useEffect(() => {
window.addEventListener("resize", logOne);
return () => {
window.removeEventListener("resize", logOne); //remove event listener on unmount
}
}, []); //empty dependency array so that function only runs on first render, so that consequent rerenders don't cause you to add more of these event listeners
Because you will keep adding listeners every render. Instead of printing "1" once, it will print it twice next time. Then 3 times on the next re-render and so on.

React useEffect forces me to add dependencies that trigger an infinite loop

Why is React forcing me with their linter plugin to add dependencies that I don't want?
For example, I want my effect to trigger only when a certan value changes, yet the linter tells me to add even functions to the dependencies, and I don't want that.
Why it forces me to do that? What do I gain from that?
/**
* Gets all items, pages, until 250th.
*/
useEffect(() => {
let mounted = true;
if (loadUntil250th && !paginationProps.complete) {
mounted && setLoading(true);
let limit = 250 - paginationProps.page * BATCH_LIMIT;
fetchListItems(paginationProps, limit, paginationProps.page * BATCH_LIMIT)
.then((results) => {
if (mounted) {
setPaginationProps({
...paginationProps,
page: 250 / BATCH_LIMIT,
autoLoad: false,
complete: paginationProps.totalItems <= 250,
});
setListItems(results.listItems);
setLoading(false);
}
})
.catch((err) => {
logger.log('LOADMORE FAILED:', err);
mounted && setPaginationProps({ ...paginationProps, complete: true });
mounted && setLoading(false);
});
}
return () => {
mounted = false;
};
}, [loadUntil250th]);
It wants this array of dependencies, which result in a infinite loop
[loadUntil250th, logger, paginationProps, setListItems]);
I want to understand why it is required, if I don't want them.
The 'exhaustive-deps' lint rule is designed to protect against stale closures, where useEffect references props or state used in the callback but not present in the dependency array. Since logger, paginationProps, and setListItems can theoretically change between renders, it's not safe to reference them inside useEffect without also including them in the dependency array to ensure you're always receiving and acting on up-to-date data. You can think of useEffect as essentially generating a snapshot of all state and props when it gets created and only updating that if one of its dependencies changes.
For instance, without including paginationProps in the dependencies list, if fetchListItems ever modifies the value of paginationProps then useEffect won't have access to that updated value until loadUntil250th changes.
As referenced in this answer, part of the issue is that your usage of useEffect() is unidiomatic. If all you're doing is subscribing to changes to loadUntil250th, you'd be better off moving this function elsewhere and calling it with your code that modifies loadUntil250th.
If you want to keep your code in the useEffect hook, you have a few options:
Assuming that paginationProps and setPaginationProps are derived from a useState hook, you can eliminate the dependency on paginationProps by passing a function to setPaginationProps instead of an object. So your code would become:
setPaginationProps(paginationProps => {
...paginationProps,
page: 250 / BATCH_LIMIT,
autoLoad: false,
complete: paginationProps.totalItems <= 250,
});
Move setListItems inside the useEffect hook if possible. This ensures that you can control whatever props/state that function depends on. If that's not possible, you have a few options. You can move the function outside the component altogether to guarantee that it doesn't depend on props or state. Alternatively, you can use the useCallback hook along with the function to control its dependencies and then list setListItems as another dependency.
It's unlikely that the logger function changes between renders, so you're probably safe keeping that inside your dependency array (although it's odd that the linter would expect that).
If you're still curious, this article is helpful at detailing how useEffect and the dependency array actually works.

Spooky Action at a Distance: Electron & React useEffect - Unable to unsubscribe from ipcRenderer events

I have encountered strange behavior when using Electron's ipcRenderer with React's useEffect.
Within my electron app, I have the following code:
import React, { useEffect } from 'react'
const electron = window.require('electron');
const ipcRenderer = electron.ipcRenderer;
...
const someValueThatChanges = props.someValue;
useEffect(() => {
const myEventName = 'some-event-name';
console.log(`Using effect. There are currently ${ipcRenderer.listenerCount(eventName)} listeners.`);
console.log(`Value that has changed: ${someValueThatChanges}.`);
ipcRenderer.addListener(myEventName, myEventHandler);
console.log('Added a new listener.');
// Should clean up the effect (remove the listener) when the effect is called again.
return () => {
ipcRenderer.removeListener(myEventName, myEventHandler)
console.log('Cleaned up event handler.');
}
}, [ someValueThatChanges ]);
function myEventHandler() {
console.log('Handled event');
}
The code above is supposed to listen to the some-event-name event fired by Electron's main process with mainWindow.webContents.send('some-event-name'); and console.log(...) a message inicating that the event was handled.
This works as expected when the effect is initially run. A listener is added, the event is raised at a later time, and the string 'Handled event' is printed to to the console. But when the someValueThatChanges variable is assigned a different value and the event is raised for a second time, the 'Handled event' string is printed out to the console twice (the old listener does not appear to have been removed).
The line with the listenerCount(eventName) call returns 0 as expected when the removeListener(...) call is included in the useEffect return/cleanup function. When the removeListener(...) call is removed, the listenerCount(eventName) call returns a value that is incremented as expected (e.g. 0, 1, 2) as listeners are not removed.
Here's the really weird part. In either case, whether or not I include the call to removeListener(...), the myEventHandler function is always called for as many times as useEffect has been run. In other words, Electron reports that there are no event listeners, but myEventHandler still seems to be called by the previous listeners. Is this a bug in Electron, or am I missing something?
Never try with ipcRenderer.addListener, But try ipcRenderer.on instead
useEffect(() => {
ipcRenderer.send('send-command', 'ping');
ipcRenderer.on('get-command', (event, data) => {
console.log('data', data);
});
return () => {
ipcRenderer.removeAllListeners('get-command');
};
}, []);
I believe, the docs changed. ipcRenderer.removeAllListeners accept single string instead of array of string Source electron issues,

How does React Hooks useCallback "freezes" the closure?

I'd like to know how does React "freezes" the closure while using the useCallback hook (and with others as well), and then only updates variables used inside the hook when you pass them into the inputs parameter.
I understand that the "freeze" may not be very clear, so I created a REPL.it that shows what I mean: https://repl.it/repls/RudeMintcreamShoutcast. Once you open the code, open your web browser console and start clicking on the count button.
How come the value outside compared to the one inside, for the same variable, is different, if they're under the same closure and referencing the same thing? I'm not familiar with React codebase and so I suppose I'm missing an under the hood implementation detail here, but I tried to think how that could work for several minutes but couldn't come up with a good understanding on how React is achieving that.
The first time the component is rendered, the useCallback hook will take the function that is passed as its argument and stores it behind the scenes. When you call the callback, it will call your function. So far, so good.
The second time that the component is rendered, the useCallback hook will check the dependencies you passed in. If they have not changed, the function you pass in is totally ignored! When you call the callback, it will call the function you passed in on the first render, which still references the same values from that point in time. This has nothing to do with the values you passed in as dependencies - it's just normal JavaScript closures!
When the dependencies change, the useCallback hook will take the function you pass in and replace the function it has stored. When you call the callback, it will call the new version of the function.
So in other words, there's no "frozen"/conditionally updated variables - it's just storing a function and then re-using it, nothing more fancy than that :)
EDIT: Here's an example that demonstrates what's going on in pure JavaScript:
// React has some component-local storage that it tracks behind the scenes.
// useState and useCallback both hook into this.
//
// Imagine there's a 'storage' variable for every instance of your
// component.
const storage = {};
function useState(init) {
if (storage.data === undefined) {
storage.data = init;
}
return [storage.data, (value) => storage.data = value];
}
function useCallback(fn) {
// The real version would check dependencies here, but since our callback
// should only update on the first render, this will suffice.
if (storage.callback === undefined) {
storage.callback = fn;
}
return storage.callback;
}
function MyComponent() {
const [data, setData] = useState(0);
const callback = useCallback(() => data);
// Rather than outputting DOM, we'll just log.
console.log("data:", data);
console.log("callback:", callback());
return {
increase: () => setData(data + 1)
}
}
let instance = MyComponent(); // Let's 'render' our component...
instance.increase(); // This would trigger a re-render, so we call our component again...
instance = MyComponent();
instance.increase(); // and again...
instance = MyComponent();
I came here with a similar, rather vague uncertainty about the way useCallback works, its interaction with closures, and the way they are "frozen" by it. I'd like to expand a bit on the accepted answer by proposing to look at the following setup, which shows the working of useCallback (the important aspect is to ignore the linter's warning, for pedagogical reasons):
function App() {
const [a, setA] = useState(0)
const incrementWithUseCallback = useCallback(() => {
// As it closes on the first time `App` is called, the closure is "frozen" in an environment where a=0, forever
console.log(a)
setA(a + 1)
}, []) // but.. the linter should complain about this, saying that `a` should be included!
const incrementWithoutUseCallback = () => {
// This will see every value of a, as a new closure is created at every render (i.e. every time `App` is called)
console.log(a)
setA(a + 1)
}
return (
<div>
<button onClick={incrementWithUseCallback}>Increment with useCallback</button>
<button onClick={incrementWithoutUseCallback}>Increment without useCallback</button>
</div>
)
}
So we clearly see that useCallback effectively "freezes" its closure at a certain moment in time, which is a concept that must be understood clearly, in order to avoid confusing problems, which are sometimes also referred as "stale closures". This article probably does a better job of explaining it than me: https://tkdodo.eu/blog/hooks-dependencies-and-stale-closures
Here's a slightly another view on example code provided by Joe Clay, which emphasizes closure context in which callback is called.
//internal store for states and callbacks
let Store = { data: "+", callback: null };
function functionalComponent(uniqClosureName) {
const data = Store.data;//save value from store to closure variable
const callback = Store.callback = Store.callback || (() => {
console.log('Callback executed in ' + uniqClosureName + ' context');
return data;
});
console.log("data:", data, "callback():", callback());
return {
increase: () => Store.data = Store.data + "+"
}
}
let instance = functionalComponent('First render');
instance.increase();
instance = functionalComponent('Second render');
instance.increase();
instance = functionalComponent('Third render');
As you see, callback without dependencies will be always executed in the closure where it was memorized by useCallback, thus 'freezing' closure.
It happens because when function for callback is created, it is created only once, during first 'render'. Later this function is re-used, and use value of data which was recorded from Store.data during first call.
In the next example you can see the closure 'freezing' logic "in essence".
let globalX = 1;
const f = (() => {
let localX = globalX; return () => console.log(localX); }
)();
globalX = 2;//does not affect localX, it is already saved in the closure
f();//prints 1

Using setTimeout to avoid re-renders in React

There is a pattern that I use often that I feel must be an anti-pattern but I don't know a better alternative.
Occasionally my components may receive one or more events that mean that a re-render is necessary. Sometimes it is the case that I do not know how many times the event handlers will be called. To prevent multiple re-renders as a result of many calls to the handlers I do something like this:
_myEventHandler() { // may be called multiple times between renders
if (!this._updateQueued) {
this._updateQueued = true;
this._updateTimer = setTimeout(() => {
this._updateQueued = false;
this.forceUpdate();
}, 0);
}
}
The problem here is I feel it can't be performant due to the latency between code stopping and the event loop starting.
A real world example of this is when I am using react-visibility-sensor and I have multiple elements change their visibility at once, I don't want to re-render for each element, instead I want just one re-render once all the updates have been received.
Is there another better way to deal with multiple calls?
BTW: if you are going to use the above hack don't forget to call clearTimout(this._updateQueued) in your componentWillUnmount
A debounce will reduce the number of times a certain piece of code is run, regardless of how often it is called. Here is a rather simple implementation.
const debounce = (callable, time) => {
let timeout;
return function() {
const functionCall = () => callable.apply(this, arguments);
clearTimeout(timeout);
timeout = setTimeout(functionCall, time);
};
};
And this is how to use it.
const debouncedIteration = debouce(() => {
console.log("Iteration"); // This will be called once every 1000 milliseconds.
}, 1000);
while (true) {
debouncedIteration();
}
You can avoid re-renders using this lifecycle method shouldComponentUpdate (as also mentioned by #fungusanthrax). Keep this inside your react component :
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}
using isEqual from lodash here, make sure to include it.
This will only re-render your component when there's a change in props or state value.
To install lodash:
npm install -S lodash
and import isEqual in your component file :
import isEqual from 'lodash/isEqual';

Resources