React componentWillUnmount hook (useEffect) with react hook (useState) array comes out empty? - reactjs

Upon componentWillUnmount I want to send an array inside a useState to a postRequest function.
My useState array:
const [array, setArray] = useState([]);
My componentWillUnmount function:
useEffect(() => {
return () => {
console.log(array); // logs "[]"
// TODO: Send to a post request to BE
}
}, [])
I do also have a "normal" useEffect that listens on my useState array. When I add three elements inside it, the console logs shows there are three elemetns inside it
useEffect(() => {
console.log('array: ', array); // logs "(3) [{…}, {…}, {…}]"
}, [array]);
How come the array is empty in my componentWillUnmount function?
Edit: Adding array to the dependecy of the useEffect will make the function execute every time that array is updated, which is not what is desired. The function should only execute upon unmount with the values of the array

It's because the value of the variable did not changed inside hook because it initiated on componentDidMount and did not received an update because the second parameter is empty.
Since you only want to trigger it once if the component unmounts and not every time the value changes:
you can use useRef to "preserve" the value without rerendering the component.
const ref = useRef();
useEffect(() => {
ref.current = array;
}, [array]);
useEffect(() => {
return function cleanup() {
console.log(ref.current); // here is your array
};
}, []);

When passing an empty dependency array as second argument to useEffect, the code inside the useEffect callback will only be run on mount. At this moment, array is still empty.
Does it work when you also add the array to the dependency array of the first useEffect like this?
useEffect(() => {
return () => {
console.log(array); // logs "[]"
// TODO: Send to a post request to BE
}
}, [array])
BTW, I think you can also combine the two like this:
useEffect(() => {
console.log('array: ', array); // logs "(3) [{…}, {…}, {…}]"
return () => {
console.log(array); // logs "[]"
// TODO: Send to a post request to BE
}
}, [array])

Related

does setInterval inside a useEffect can recognize an updated state?

I'm working on an app which requires a user location every X seconds. When the component is mounted, the interval starts and fetch the GPS location and compare it to the old one which is saved on the component state.
The thing is, the interval is comparing only to the default state of the variable, so if the new value is different than the old one, a setter is called to change that value.
I used useEffect on the location var so whenever it changes, to just print it and it does correctly by the new value.
but the interval keeps using the default value given in useState hook.
What is it that I'm missing here.
An example of my code is below:
const [currentLocation, setCurrentLocation] = useState(null);
let locationInterval = null;
useEffect(() => {
locationInterval = setInterval(async () => {
console.log("IN INTERVAL");
navigator.geolocation.getCurrentPosition((location, error) => {
if (location) {
location = [location.coords.latitude, location.coords.longitude];
/// currentLocation is not updating here for some reason
if (JSON.stringify(location) !== JSON.stringify(currentLocation)) {
alert(
`the new location in interval ${location} old location: ${currentLocation}`
);
setCurrentLocation(location);
}
} else {
console.log(error);
}
});
}, 15000);
}, [map]);
useEffect(() => {
return () => {
clearInterval(locationInterval);
};
}, []);
useEffect(() => {
/// currentLocation is updated here from the setInterval
console.log("newlocation", currentLocation);
}, [currentLocation]);
The reason is because currentLocation value is not given as a dependency in the useEffect, therefore that useEffect callback function only has access to the original useState value.
May I ask are there any React warning on your terminal where you run your react app?
Something like
React Hook useEffect has a missing dependency: 'currentLocation'
But on the side node, if you add currentLocation to the dependency,since because you are updating currentLocation in your useEffect and if it's updated, it will re-run the useEffect callback again.
You can not get an updated state in setInterval because callback inside useEffect hook only called when its dependency array updates([map]).
When it is called currentLocation is passed as a parameter to setInterval's callback. Which is the default value for useState at that time.
To prevent this you can do this.
const [currentLocationRef] = useRef(null);
useEffect(() => {
locationInterval = setInterval(async () => {
//....
if (JSON.stringify(location) !== JSON.stringify(currentLocationRef.current)) {
currentLocationRef.current = location;
}
}, 15000);
return () => {
clearInterval(locationInterval);
};
}, [map, currentLocationRef]);
Also, you should return clearInterval in the same useEffect Callback.
The reason behind this is currentLocationRef.current is different from currentLocation. The first one is the getter function and the second one is value. That's why the first one is still able to access updated value while the second one cannot.

React functional component access state in useeffect

I've gote some react component like below. I can use "messages" in return, but if I try to access messages inside some function, or useEffect, as in example, I always become initial value. How can I solve it in functional component? Thanks
const Messages = () => {
const { websocket } = useContext(WebsocketsContext);
let [ messages, setMessages ] = useState([]);
useEffect(() => {
getMessages()
.then(result => {
setMessages(result);
})
}, []);
useEffect(() => {
if(websocket != null){
websocket.onmessage = (msg) => {
let wsData = JSON.parse(msg.data);
if(wsData.message_type == 'Refresh'){
console.log(messages)
};
};
};
}, [websocket]);
return(
<div>...</div>
);
};
export default Messages;
Looks like you have encountered a stale closure
the useEffect with [websockets] in its dependency array will only ever "update" whenever the websocket reference/value changes. Whenever it does, the function will have created a "closure" around messages at that point in time. Thus, the value of messages will stay as is within that closure. If messages updates after websocket has been created, it will never update the value of "messages" within the onmessage callback function.
To fix this, add "messages" to the dependency array. [websockets, messages]. This will ensure the useEffect callback always has the latest state of messages, and this the onmessage function will have the latest state of messages.
useEffect(() => {
if(websocket != null){
websocket.onmessage = (msg) => {
let wsData = JSON.parse(msg.data);
if(wsData.message_type == 'Refresh'){
console.log(messages)
};
};
};
}, [websocket, messages]);
It's because your getMessages() is an async function. The order is as follows: component mounts initially and values are initialized -> componentDidMount() is invoked meaning your getMessages() is invoked (an async function!) -> your webaocket is initialized and invokes the second useEffect, which reads the initial value of messages -> your getMessages gets its response and sets the messages accordingly.
To make it work as intended, make the second useEffect's dependency array as [websocket, messages].

how to use the useEffect hook on component unmount to conditionally run code

For some odd reason the value of props in my "unmount" useEffect hook is always at the original state (true), I can console and see in the devtools that it has changed to false but when the useEffect is called on unmount it is always true.
I have tried adding the props to the dependancies but then it is no longer called only on unmount and does not serve it's purpose.
Edit: I am aware the dependancy array is empty, I cannot have it triggered on each change, it needs to be triggered ONLY on unmount with the update values from the props. Is this possible?
React.useEffect(() => {
return () => {
if (report.data.draft) { // this is ALWAYS true
report.snapshot.ref.delete();
}
};
}, []);
How can I conditionally run my code on unmount with the condition being dependant on the updated props state?
If you want code to run on unmount only, you need to use the empty dependency array. If you also require data from the closure that may change in between when the component first rendered and when it last rendered, you'll need to use a ref to make that data available when the unmount happens. For example:
const onUnmount = React.useRef();
onUnmount.current = () => {
if (report.data.draft) {
report.snapshot.ref.delete();
}
}
React.useEffect(() => {
return () => onUnmount.current();
}, []);
If you do this often, you may want to extract it into a custom hook:
export const useUnmount = (fn): => {
const fnRef = useRef(fn);
fnRef.current = fn;
useEffect(() => () => fnRef.current(), []);
};
// used like:
useUnmount(() => {
if (report.data.draft) {
report.snapshot.ref.delete();
}
});
The dependency list of your effect is empty which means that react will only create the closure over your outer variables once on mount and the function will only see the values as they have been on mount. To re-create the closure when report.data.draft changes you have to add it to the dependency list:
React.useEffect(() => {
return () => {
if (report.data.draft) { // this is ALWAYS true
report.snapshot.ref.delete();
}
};
}, [report.data.draft]);
There also is an eslint plugin that warns you about missing dependencies: https://www.npmjs.com/package/eslint-plugin-react-hooks
Using custom js events you can emulate unmounting a componentWillUnmount even when having dependency. Here is how I did it.
Problem:
useEffect(() => {
//Dependent Code
return () => {
// Desired to perform action on unmount only 'componentWillUnmount'
// But it does not
if(somethingChanged){
// Perform an Action only if something changed
}
}
},[somethingChanged]);
Solution:
// Rewrite this code to arrange emulate this behaviour
// Decoupling using events
useEffect( () => {
return () => {
// Executed only when component unmounts,
let e = new Event("componentUnmount");
document.dispatchEvent(e);
}
}, []);
useEffect( () => {
function doOnUnmount(){
if(somethingChanged){
// Perform an Action only if something changed
}
}
document.addEventListener("componentUnmount",doOnUnmount);
return () => {
// This is done whenever value of somethingChanged changes
document.removeEventListener("componentUnmount",doOnUnmount);
}
}, [somethingChanged])
Caveats: useEffects have to be in order, useEffect with no dependency have to be written before, this is to avoid the event being called after its removed.

Infinite re-render in functional react component

I am trying to set the state of a variable "workspace", but when I console log the data I get an infinite loop. I am calling the axios "get" function inside of useEffect(), and console logging outside of this loop, so I don't know what is triggering all the re-renders. I have not found an answer to my specific problem in this question. Here's my code:
function WorkspaceDynamic({ match }) {
const [proposals, setProposals] = useState([{}]);
useEffect(() => {
getItems();
});
const getItems = async () => {
const proposalsList = await axios.get(
"http://localhost:5000/api/proposals"
);
setProposals(proposalsList.data);
};
const [workspace, setWorkspace] = useState({});
function findWorkspace() {
proposals.map((workspace) => {
if (workspace._id === match.params.id) {
setWorkspace(workspace);
}
});
}
Does anyone see what might be causing the re-render? Thanks!
The effect hook runs every render cycle, and one without a dependency array will execute its callback every render cycle. If the effect callback updates state, i.e. proposals, then another render cycle is enqueued, thus creating render looping.
If you want to only run effect once when the component mounts then use an empty dependency array.
useEffect(() => {
getItems();
}, []);
If you want it to only run at certain time, like if the match param updates, then include a dependency in the array.
useEffect(() => {
getItems();
}, [match]);
Your use of useEffect is not correct. If you do not include a dependency array, it gets called every time the component renders. As a result your useEffect is called which causes setProposals then it again causes useEffect to run and so on
try this
useEffect(() => {
getItems();
} , []); // an empty array means it will be called once only
I think it's the following: useEffect should have a second param [] to make sure it's executed only once. that is:
useEffect(() => {
getItems();
}, []);
otherwise setProposal will modify the state which will trigger a re-render, which will call useEffect, which will make the async call, which will setProposal, ...

Using useEffect cleanup function to store form values in localStorage

I am using the useEffect cleanup function to store my formik values in localStorage. The strange part is the values stored are stale initial values and not the current form values. Here is the code:
console.log(values)
React.useEffect(() => {
// do stuff
return function() {
console.log(values)
// store form data on unmount
localStorage.setItem("formValues", JSON.stringify(values))
};
}, []);
The values logged outside the useEffect function are current, the one logged (and stored) within the cleanup function are stale initial values. I though the function would be lazily evaluated so the values variable would have the current value. As a shot in the dark, I also tried using a getter function for the form values. But still the old stale values. What's happening here?
If you want cleanup just on component unmount this is what you want:
const valuesRef = useRef(values);
useEffect(() => {
valuesRef.current = values;
}, [values]);
useEffect(() => {
// do stuff
return function() {
console.log(valuesRef.current)
// store form data on unmount
localStorage.setItem("formValues", JSON.stringify(valuesRef.current))
};
}, []);
Beacuase you want just cleanup and update localstorage on component unmount, you should pass empty array ([]) to useEffect.
If you pass an empty array ([]), the props and state inside the effect will always have their initial values (https://reactjs.org/docs/hooks-effect.html). It's because each effect “belongs” to a particular render that in this case it's the first render when component is mounted.
But it might be better to update localstorage on each values change and in this case you only need to pass [values] to useEffect.
There are two ways to fix this problem.
pass values inside the [] of useEffect.
React.useEffect(() => {
// do stuff
return function() {
console.log(values)
// store form data on unmount
localStorage.setItem("formValues", JSON.stringify(values))
};
}, [values]);
But in this way, it will save everytime values is changed
So not appropriate if you only save once when the component is unmounted
Second method (useRef)
const formikRef = React.useRef();
formikRef.current = useFormik({
initialValues: ...,
onSubmit: ...,
validationSchema: ...
});
React.useEffect(() => {
// do stuff
return () => {
console.log(formikRef.current.values);
// store form data on unmount
localStorage.setItem(
"formValues",
JSON.stringify(formikRef.current.values)
);
};
}, []);
const formik = formikRef.current;

Resources