How does "unsubscribe" in the useEffect cleanup function actually work? - javascript

In many code examples I see something like this:
const [items, setItems] = useState([]);
useEffect(() => {
setItems(store.getState().items.length);
const unsubscribe = store.subscribe(() => {
setItems(store.getState().items.length);
});
return unsubscribe; // <-- huh?
}, []);
My question is; how does returning a reference to the subscription unsubscribe from it?

const unsubscribe = store.subscribe(() => {
setItems(store.getState().items.length);
});
This call to store.subscribe immediately creates a subscription with the redux store, and then redux returns a function to you. This returned function is an unsubscribe function which knows how to tear down the subscription. If you're curious, here's the source code where they create that function.
return unsubscribe;
By returning the unsubscribe function, you tell react "hey, when it's time to tear down this effect, please run unsubscribe". React will then call it at the appropriate time: either when the component unmounts, or when the dependencies on the effect change.

.subscribe returns a function that unsubscribes the change listener. That is what you return from useEffect callback and is called as cleanup.
It gets called automatically. So questioning that is like questioning how does useEffect run after renders. React takes care of this so you do not have to worry.

the cleanup function runs before every render. So every render it will run before the useEffect. It will also be executed when the component unmounts, and then of course the rest of the useEffect will not run anymore

The useEffect cleanup function allows applications to prevent having memory leaks by 'cleaning up' any effects. In a basic example you have this structure
useEffect(() => {
effect
return () => {
cleanup
}
}, [input])
Here you will see the useEffect method will run an effect whenever an input in the dependancy array gets updated. So if your useEffect returns a function once it's time to unmount (or update) it'll run that function.
As for how it works think of the logic, when the component mounts we run the code and store the result. When it comes time to unmount or rerender the stored result will run first cleaning up any logic.
Now more specific to your code I don't think it makes sense, you should investigate the store.subscribe method and that will most likely answer your question.
EDIT
after seeing the Link to the code you'll see the initial question had a memory leak in it.

Related

React - Updating data in dispatch from useEffect

I am trying updating data in dispatch in useEffect but showing warning in console
React Hook useEffect has missing dependencies: 'dispatch', 'id', and 'state.selectedHotel'. Either include them or remove the dependency array react-hooks/exhaustive-deps
code
import { GlobalContext } from "../../../context/globalContext";
const HotelDetail = () => {
const [state, dispatch] = useContext(GlobalContext);
const { id } = useParams();
useEffect(() => {
const hotelData = async () => {
try {
let response = await ServiceGetAllHotels();
let hotel = response.hotels.filter(hotel => {
return hotel.hotelUserName === id;
});
dispatch({
type: "UPDATE",
payload: { selectedHotel: hotel[0] }
});
}catch(){}
};
}, [])
};
But warning message disappear when I add this (below code)
useEffect(() => {
.....
}, [dispatch, state.selectedHotel, id])
I dont understand why this error/warning , why error disappear when I add this ? Please help Can I go with this code?
Its not an error but a warning that can save you from bugs because of useEffect hook not running when it was supposed to.
useEffect hook, by default, executes after:
the initial render
each time a component is re-rendered
Sometimes we don't want this default behavior; passing a second optional argument to useEffect hook changes the default execution behavior of useEffect hook. Second argument to useEffect hook is known as its dependency array that tells React when to execute the useEffect hook.
Run "useEffect" once, after the initial render
We can achieve this by passing an empty array as the second argument to the useEffect hook:
useEffect(() => {
// code
}, []);
This effect will only execute once, similar to componentDidMount in class components.
Run "useEffect" everytime any of its dependency changes
When the code inside the useEffect depends on the state or a prop, you sometimes want useEffect to execute every time that piece of state or prop changes.
How can we tell React to run the effect every time a particular state or prop changes? By adding that state or prop in the dependency array of the useEffect hook.
Example:
Imagine a Post component that receives post id as a prop and it fetches the comments related to that post.
You might write the following code to fetch the comments:
useEffect(() => {
fetch(`/${props.postId}`)
.then(res => res.json())
.then(comments => setComments(comments))
.catch(...)
}, []);
Problem with the above code:
When the Post component is rendered for the first time, useEffect hook will execute, fetching the comments using the id of the post passed in as the argument.
But what if the post id changes or the post id is not available during the first render of the Post component?
If post id prop changes, Post component will re-render BUT the post comments will not be fetched because useEffect hook will only execute once, after the initial render.
How can you solve this problem?
By adding post id prop in the dependency array of the useEffect hook.
useEffect(() => {
fetch(`/${props.postId}`)
.then(res => res.json())
.then(comments => setComments(comments))
.catch(...)
}, [props.postId]);
Now every time post id changes, useEffect will be executed, fetching the comments related to the post.
This is the kind of problem you can run into by missing the dependencies of the useEffect hook and React is warning you about it.
You should not omit any dependencies of the useEffect hook or other hooks like: useMemo or useCallback. Not omitting them will save you from such warnings from React but more importantly, it will save you from bugs.
Infinite loop of state update and re-render
One thing to keep in mind when adding dependencies in the dependency array of the useEffect is that if your are not careful, your code can get stuck in an infinite cycle of:
useEffect --> state update ---> re-render --> useEffect ....
Consider the following example:
useEffect(() => {
const newState = state.map(...);
setState(data);
}, [state, setState]);
In the above example, if we remove the state from the dependency array, we will get a warning about missing dependencies and if we add state in the array, we will get an infinite cycle of state update and re-render.
What can we do?
One way is to skip the state as a dependency of the useState hook and disable the warning using the following:
// eslint-disable-next-line react-hooks/exhaustive-deps
Above solution will work but it's not ideal.
Ideal solution is to change your code in such a way that allows you to remove the dependency that is causing the problem. In this case, we can simply use the functional form of the setState which takes a callback function as shown below:
useEffect(() => {
setState(currState => currState.map(...));
}, [setState]);
Now we don't need to add state in the dependency array - problem solved!
Summary
Don't omit the dependencies of the useEffect hook
Be mindful of the infinite cycle of state update and re-render. If you face this problem, try to change your code in such a way that you can safely remove the dependency that is causing the infinite cycle
The useEffect hook accepts two arguments. The first one is a classic callback and the second one is an array of so called "dependencies".
The hook is designed to execute the callback immediately after component has been mount (after elements have been successfully added to the real DOM and references are available) and then on every render if at least one of the values in the dependencies array has changed.
So, if you pass an empty array, your callback will be executed only once during the full lifecycle of your component.
It makes sense if you think about it from a memory point of view. Each time that the component function is executed, a new callback is created storing references to the current execution context variables. If those variables change and a new callback is not created, then the old callback would still use the old values.
This is why "missing dependencies" is marked as a warning (not an error), code could perfectly work with missing dependencies, sometimes it could be also intentional. Even if you can always add all dependencies and then perform internal checks. It is a good practice to pass all your dependencies so your callback is always up to date.

How to get the correct state in an useEffect hook, after another useEffect hook modifies the same state?

I have two useEffect hooks, which will both run on change in startTime/endTime state.
useEffect(() => {
if (selectedProduct) {
fetch(...)
.then(res => res.json())
.then(res => setMyState(res.data));
}
}, [selectedProduct, startTime, endTime]);
useEffect(() => {
if(selectedStep){
console.log(myState); //this runs twice, once for the old state and once for the new state, after above useEffect's setState is complete
}
}, [selectedStep, startTime, endTime, myState]);
I understand from looking around that this is probably because React does some sort of batching for performance gain such that setState is async in nature. Also, the fetch call would be async too, so it will take time to set the new state.
From what I have searched, I see that there is a callback for class based React components using setState but as I am using hooks, I am not sure what to do. I can move the second useEffect code in the first one, but that would require some ugly if-else conditions as I need to check if selectedStep has not changed, and then I can use the res.data directly.
Is there a better way of doing this?
It's not batching of operations or anything like that, it's just that that's how it works: When startTime or endTime changes, each of those hooks is executed, and since the first changes myState, your second one gets called again for that change.
A few options for you
Combine the hooks as you described, even if it involves conditional logic.
Depending on what you're doing with myState, you might make the second hook use the callback form of setMyState which receives the current state value as a parameter.
(If possible) Remove the dependencies on other state items in your second useEffect so that it only gets executed when myState changes
Despite your saying it'll involve some if/else, I'd lean toward #1 if you can't do #3.

React - Effect of closures within React application

I have stumbled upon an issue that is very weird for me, but most probably is very simple to explain.
Demo
Let's assume the following React component
import React, { useState, useEffect, useCallback } from "react";
export default function App() {
const [test, setTest] = useState();
const doSomething = () => {
// TODO: Why does this returns the inital state value? Hoisting?
console.log(test);
};
const doSomethingWithCallback = useCallback(doSomething, [test]);
useEffect(() => {
setTest("asas");
window.setTimeout(() => doSomething(), 2000);
document.addEventListener("click", doSomethingWithCallback);
return () => {
document.removeEventListener("click", doSomethingWithCallback);
};
}, [doSomethingWithCallback]);
return (
<div className="App">
<h1>Click anywhere</h1>
</div>
);
}
(cf. CodeSandbox)
Question
Look at the TODO comment in the code.
Why does doSomething console logs the state test as it is initially is set, namely undefined whereas the callback variant is returning it with the "real" current state when it is called?
Is this some kind of hoisting or performance optimization React is doing?
This doesn't have much of anything to do with hoisting. setTest is causing a rerender which will cause the useCallback to evaluate it's callback since it's dependency changed.
Explaining why the other function call returns the initial value is a little more mysterious, but I think it has to do with the value of test when you registered the call to setTimeout.
Any setState is asynchronous, you can not guarantee the value in state will line up with what you expect if you set the state and then synchronously try to reference state right afterwards. To the contrary, it most likely won't, I'm not fully informed, but I believe more or less no asynchronous work is done until the queue of synchronous work is completely empty.
As for your actual question, "are state values hoisted?" yes they are, like all values in Javascript. However react state hook values differ slightly in that the order of their execution must always be the same between renders, so you have to maintain at least that much (i.e. no conditional hook instantiation may exist).

React useEffect hook dispatch actions on chain

First of all i am new to react hooks. hence please bear if it's silly.
So, i want to dispatch actions one after another in useEffect hook (on Load).
Also my second dispatch needs data from first dispatch.
Here is what i have written which is not working.
const userData = useSelector((state) => state.user.data);
useEffect(() => {
dispatch(fetchUser("someUserId")).then(() => {
dispatch(fetchProjects("someUserId", userData.settings.startDate));
});
}, []); // on load
In the above code, userData.settings is fetched from first dispatch. When i run the code i get an error saying userData.settings is not defined.
Please let me know, what is wrong here.
Thanks in advance

What React useCallback do when I pass. an object in first argument

I am fixing an infinite loop when using react with useEffect and useCallback.
const fetchApi =useCallback(()=>{
setIsLoading(true)
fetchDataFunction({
resolve(res) {
setFetchData(fromJS(res))
setIsLoading(false)
}
})
},[param])
useEffect(()=>{
fetchApi()
},[fetchApi])
It is causing an infinite loop. However, if I change the code as shown below
const fetchApi = useCallback({
fetchApiFunc() {
setIsLoading(true)
fetchDataFunction({
resolve(res) {
setFetchData(fromJS(res))
setIsLoading(false)
}
})
}
},[params])
const { fetchApiFunc } = fetchApi
useEffect(() => {
fetchApiFunc()
}, [fetchApiFunc])
the problem is fixed. But I still do not understand what it does when the object in first params in useCallback.
Thank you.
If you start using React-Hooks, and your component might need a life cycle method at some point. And, that is when you start using useEffect() (a.k.a Effect Hook). Then boom!!, you have encountered an infinite loop behavior, and you have no idea why the hell is that. If that happens, this article will explain to you why, and how can you prevent.
Where is the problem?
The “useEffect()”, will run after the initial render, then invoke the "fetchApiFunc()”.
Inside the "fetchApiFunc()”, it will update the state “name” on line Then it will trigger the component to re-render again.
As a result, “useEffect()” will run again and update the state. Next, the whole process repeats again, and you're trapped inside an infinite loop.
“You can tell React to skip applying an effect if certain values haven’t changed between re-renders. To do so, pass an array as an optional second argument to useEffect”, from the official here: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Categories