Do react query result objects go stale due to closures? - javascript

I have always felt like I have a pretty solid understanding of how JS closures work, but since using ReactJS for the last couple of months there is something I can't quite make sense of.
In the below example, will the query result object getData be stale if used in the success callback for the other query?
My closure instincts tell me yes, but my reactjs instincts tell me no for some reason and I can't make sense of why it works the way that it does.
All help is appreciated!
/*
* Custom query hooks
*/
function useGetData(someId, opts){
return useQuery(['getData', someId],
getDataQueryFn,
{
...opts,
},
);
}
function useOtherData(someId, opts){
return useQuery(['otherData', someId],
otherDataQueryFn,
{
...opts,
},
);
}
/*
* React component
*/
function MyComponent(){
const getData = useGetData("1");
const otherData = useOtherData("1", {
onSuccess: () => {
// QUESTION: if I use getData here, won't it be stale due to closure? why or why not?
}
});
// render and stuff...
}

It doesn't go stale. Here's a codesandbox that closures over a counter and the fetch needs 5 seconds to complete. relevant code from the sandbox:
const [count, inc] = React.useReducer((val) => val + 1, 0);
const { data } = useQuery(
"repoData",
async () => {
await waitFor(5000);
return Promise.resolve({ name: "foo " });
},
{
onSuccess: () => console.log(count)
}
);
if you click the counter a couple of times in the first 5 seconds, the latest count will still be logged. It doesn't really matter if you closure over local state or over some other query.
The reason is likely that react-query will only trigger a re-render after the Promise is successful (it's a simple forced re-render via useState under the hood), and at that time, react already "sees" the latest value, not the value from the time when the query started to fetch.

It depends on useQuery implementation. However I am pretty sure it wont use stale data. I expect following order:
Each time result of useGetData is updated - re-render happens, due to some internal state update
new onSuccess callback (capturing latest getData result in closure) is passed via props to useOtherData and picked up by library to handle next update

Related

Why is React state not updated inside functions? [duplicate]

This question already has answers here:
React - useState - why setTimeout function does not have latest state value?
(2 answers)
Closed 7 months ago.
I have a component that renders a table with objects. This component shows a button that, when pressed, sends the parent a specific object. The parent must set it in the state to display some graphical stuff. The rendering is working correctly, what I don't understand is why I am getting an outdated value after setting the state correctly.
It's not a race condition, React is simply ignoring the updated value of a variable, even when it re-renders the component correctly.
A minimal example:
import { useState } from "react";
import { SomeComponent } from "./SomeComponent";
export default function App() {
const [currentID, setCurrentID] = useState(null);
function getData() {
console.log("Getting data of: ", currentID); // PROBLEM: this is null
}
function setAndRetrieveData(value) {
setCurrentID(value);
// Just to show the problem and discard race conditions.
setTimeout(() => {
getData();
}, 1500);
}
return (
<div className="App">
<h1>Current ID: {currentID}</h1> {/* This works fine */}
<SomeComponent getInfoFor={setAndRetrieveData} />
</div>
);
}
SomeComponent:
export function SomeComponent(props) {
const randomID = 45;
return <button onClick={() => props.getInfoFor(randomID)}>Get info</button>;
}
Even with solutions like useStateCallback the problem persists.
Is there a way to do this without having to use the awful useEffect which is not clear when reading the code? Because the logic of the system is "when this button is pressed, make a request to obtain the information", using the hook useEffect the logic becomes "when the value of currentID changes make a request", if at some point I want to change the state of that variable and perform another action that is not to obtain the data from the server then I will be in trouble.
Thanks in advance
I think this is an issue with the way Javascript closures work.
When you execute a function, it gets bundled with all the data that pertains to it and then gets executed.
The issue is that you call this:
setTimeout(() => {
getData();
}, 1500);
inside setAndRetrieveData(value).
Even though it's inside a setTimeout, the getData() function has been bundled with the information it needs (currentID) at that point in time, not when it actually runs. So it gets bundled with the currentId before the state update takes place
Unfortunately, I would recommend using useEffect. This is the best way to ensure you avoid issues like this and any potential race conditions. Hopefully someone else can provide a different approach!
when setAndRetrieveData is called it sets a state that leads to the component being rerendered to reflect the new state. When the timeout finishes The function getData was created in the previous render. And thus only has access to the state variable from the previous render. That now is undefined.
what you could try is using a useEffect hook that that listens to changes of
currentID.
useEffect(() => {
const timeoutId = setTimeout(() => {
// Do something with the updated value
},1000);
return () => {
// if the data updates prematurely
// we cancel the timeout and start a new one
clearTimeout(timeoutId);
}
},[currentID])

How can I update a state variable from a promise?

I am trying to determine if a customer has an active subscription or not. To do this I am utilizing the following code:
const stripe = require('stripe')('some-api-key');
export default function Example(){
// the user will automatically be considered non-subbed by default
const [isSubscriber, setIsSubscriber] = useState(false)
// grab the customer id from stripe
async function get_customer_id() {
const customers = await stripe.customers.search({
query: `metadata[\'some-meta-data-key\']:\'some-meta-data-value\'`
});
return customers.data[0]['id']
}
// grab the list of active subscriptions from stripe
async function customer_is_subscriber(){
const subs = await stripe.subscriptions.list({
status: 'active',
});
return subs
}
// determine if the customer id is in the list of active subscriptions.
// return true if so, false otherwise
async function test_equality(){
const customer_id = await get_customer_id();
const subbed = await customer_is_subscriber();
const answer = subbed.find(sub => sub.customer === customer_id)
return !!answer;
}
useEffect( () => {
async function load_result() {
const promise_function_return = await test_equality()
setIsSubscriber(promise_function_return)
}
load_result();
}, [isSubscriber]);
return (
// some react code
)
}
I have been able to successfully get all of my other functions where I am doing the comparisons for if a user is a subscriber but where I am having an issue is updating the state value (e.g. true if they are subbed, false otherwise).
I found some good past questions on this specific topic such as:
here The useState set method is not reflecting a change immediately
here: setState inside Promise in React
and here: setState inside a Promise function in a useEffect with hooks?
but I just have not been able to get it working correctly. This is currently the closest I have been able to get to solving this problem.
Currently your code says that, when isSubscriber changes, it should check if the user is a subscriber and update the isSubscriber state... so it's a chicken and egg problem. It won't set isSubscriber until isSubscriber gets set.
I think you want to change }, [isSubscriber]); to }, []); so that that code executes when the component first loads (not when isSubscriber changes).
The useEffect hook will always run on mount regardless of if there is anything in its dependency array. This means that your useEffect will work as is, and will run onMount as well as when isSubscriber changes:
useEffect( () => {
async function load_result() {
const promise_function_return = await test_equality()
setIsSubscriber(promise_function_return)
}
load_result();
}, [isSubscriber]);
To verify this, you can check out this codesandbox example. The useEffect looks just like yours, and you will notice that isSubscriber is initially set to false, but is updated to true after 3 seconds.
There's still an adjustment you may want to make even though that part appears to work ok. With isSubscriber in the dependency array, the function in your useEffect will be called any time isSubscriber changes. This probably not what you want, since this function doesn't actually depend on isSubscriber, but actually sets isSubscriber. In this case, that means test_equality() will be run on initial mount and then one more time after it sets isSubscriber, which is unnecessary.
This blog post explains the useEffect dependency array really well.
You can fix this by removing isSubscriber from the dependency array, like this:
useEffect(() => {
console.log("in useEffect");
async function load_result() {
const promise_function_return = await test_equality();
setIsSubscriber(promise_function_return);
}
load_result();
}, [isSubscriber]);
Since you mentioned the state value is not getting updated, there must be another issue going on in either get_customer_id() or customer_is_subscriber(). It would be good to double check and make sure the stripe api calls are working as expected.

React state inside a function is not changing even after calling it with a delay of (5 seconds)

In react I am using functional component and I have two functions (getBooks) and (loadMore)
getBooks get data from an endPoint. But when I call loadMore function on button click inside the getBooks function (loadMoreClicked) is not changed it uses the previous state even after calling it with a delay of (5 seconds). But when I call loadMore again the state changes and everything works fine.
can someone explain why the (loadMoreClicked) on the initial call to (getBooks) didn't update
even calling it after 5 seconds delay.
function component() {
const [loadMoreClicked, setLoadMore] = useState(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`; //this is my end point
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(loadMoreClicked); //the (loadMoreClicked) value is still (false) after (5 sec)
})
.catch(err => {
console.log(err);
});
};
const loadMore = () => {
setLoadMore(true); //here i am changing (loadMoreClicked) value to (true)
setTimeout(() => {
getBooks(); // i am calling (getBooks()) after 5 seconds.
}, 5000);
};
return (
<div>
<button onClick={() => loadMore()}>loadMore</button> //calling (loadMore)
function
</div>
);
}
There's two things going on:
getBooks() is using const values that are defined in the surrounding function. When a function references const or let variables outside of its definition, it creates what's called a closure. Closures take the values from those outer variables, and gives the inner function copies of the values as they were when the function was built. In this case, the function was built right after the state was initially called, with loadMoreClicked set to false.
So why didn't setLoadMore(true) trigger a rerender and rewrite the function? When we set state, a rerender doesn't happen instantaneously. It is added to a queue that React manages. This means that, when loadMore() is executed, setLoadMore(true) says "update the state after I'm done running the rest of the code." The rerender happens after the end of the function, so the copy of getBooks() used is the one built and queued in this cycle, with the original values built in.
For what you're doing, you may want to have different functions called in your timeout, depending on whether or not the button was clicked. Or you can create another, more immediate closure, based on whether you want getBooks() to consider the button clicked or not, like so:
const getBooks = wasClicked => // Now calling getBooks(boolean) returns the following function, with wasClicked frozen
() => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(wasClicked); // This references the value copied when the inner function was created by calling getBooks()
})
.catch(err => {
console.log(err);
});
}
...
const loadMore = () => {
setLoadMore(true);
setTimeout(
getBooks(true), // Calling getBooks(true) returns the inner function, with wasClicked frozen to true for this instance of the function
5000
);
};
There is a third option, which is rewriting const [loadMoreClicked, setLoadMore] to var [loadMoreClicked, setLoadMore]. While referencing const variables freezes the value in that moment, var does not. var allows a function to reference the variable dynamically, so that the value is determined when the function executes, not when the function was defined.
This sounds like a quick and easy fix, but it can cause confusion when used in a closure such as the second solution above. In that situation, the value is fixed again, because of how closures work. So your code would have values frozen in closures but not in regular functions, which could cause more confusion down the road.
My personal recommendation is to keep the const definitions. var is being used less frequently by the development community because of the confusion of how it works in closures versus standard functions. Most if not all hooks populate consts in practice. Having this as a lone var reference will confuse future developers, who will likely think it's a mistake and change it to fit the pattern, breaking your code.
If you do want to dynamically reference the state of loadMoreClicked, and you don't necessarily need the component to rerender, I'd actually recommend using useRef() instead of useState().
useRef creats an object with a single property, current, which holds whatever value you put in it. When you change current, you are updating a value on a mutable object. So even though the reference to the object is frozen in time, it refers to an object that is available with the most current value.
This would look like:
function component() {
const loadMoreClicked = useRef(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
console.log(loadMoreClicked.current); // This references the property as it is currently defined
})
.catch(err => {
console.log(err);
});
}
const loadMore = () => {
loadMoreClicked.current = true; // property is uodated immediately
setTimeout(getBooks(), 5000);
};
}
This works because, while loadMoreClicked is defined as a const at the top, it is a constant reference to an object, not a constant value. The object being referenced can be mutated however you like.
This is one of the more confusing things in Javascript, and it's usually glossed over in tutorials, so unless you're coming in with some back-end experience with pointers such as in C or C++, it will be weird.
So, for what you are doing, I'd recommend using useRef() instead of useState(). If you really do want to rerender the component, say, if you want to disable a button while loading the content, then reenable it when the content is loaded, I'd probably use both, and rename them to be clearer as to their purpose:
function component() {
const isLoadPending = useRef(false);
const [isLoadButtonDisabled, setLoadButtonDisabled] = useState(false);
const getBooks = () => {
const endPoint = `http://localhost/getBooks`;
axios
.get(endPoint, {
params: newFilters
})
.then(res => {
if (isLoadPending.current) {
isLoadPending.current = false:
setLoadButtonDisabled(false);
}
})
.catch(err => {
console.log(err);
});
};
const loadMore = () => {
isLoadPending.current = true;
setLoadButtonDisabled(true);
setTimeout(getBooks(), 5000);
};
}
It's a little more verbose, but it works, and it separates your concerns. The ref is your flag to tell your component what it's doing right now. The state is indicating how the component should render to reflect the button.
Setting state is a fire-and-forget operation. You won't actually see a change in it until your component's entire function has executed. Keep in mind that you get your value before you can use the setter function. So when you set state, you aren't changing anything in this cycle, you're telling React to run another cycle. It's smart enough not to render anything before that second cycle completes, so it's fast, but it still runs two complete cycles, top to bottom.
you can use the useEffect method to watch for loadMoreClicked updates like componentDidUpdate lifecycle method and call the setTimeout inside that,
useEffect(() => {
if(loadMoreClicked){
setTimeout(() => {
getBooks();
}, 5000);
}
}, [loadMoreClicked])
this way only after the loadMoreClicked is changed to true we are calling the setTimeout.
This boils down to how closures work in JavaScript. The function given to setTimeout will get the loadMoreClicked variable from the initial render, since loadMoreClicked is not mutated.

I can't understand how this code(for React/Redux) works

I have a question for React/Redux.
I am making a web application. And it works perfectly in the way I want it to work without any problems. Even though I don't understand it. The part I don't understand is this part.
This is my code.
const myApps = () => {
let myInfo = {};
const form = [];
form.push(<div>what</div>);
myInfo = form.reduce((prev, element, index) => {
prev[index] = {isTrue : false};
return prev;
}, {});
}
useEffect(() => {
uploadToRedux(myInfo); // this is redux dispatch. I upload to redux "only" here.
}, []);
myInfo[0].isTrue = true;
if (myInfo[0].isTrue) console.log('Correct!');
else console.log('Incorrect!');
I'm sending myInfo to Redux only if it's componentDidMount by using useEffect. By the way, When I switch myInfo[0].isTrue = true, the state in the redux changes simultaneously. Even though I didn't send a dispatch!
Of course, this is what I want, but is there any change in the state in redux even if I didn't send the dispatch? How could this be?
Notice that you are passing a function to useEffect() as callback.
() => {
uploadToRedux(myInfo); // this is redux dispatch. I upload to redux "only" here.
}
useEffect() will execute immediately but the anonymous function (callback) will be scheduled to be executed by JS runtime (i.e. ready to be executed but in fact not executed yet).
By the time the callback is about to execute and update values in redux, JS runtime will already have executed the lines after useEffect() i.e. the following lines of code will be executed before redux update:
myInfo[0].isTrue = true;
if (myInfo[0].isTrue) console.log('Correct!');
else console.log('Incorrect!');
So when JS runtime will execute following line:
uploadToRedux(myInfo);
Value of myInfo[0].isTrue will already be updated and same will go to redux.
The phenomena is called Callback Hell. To know more about it, read this: http://callbackhell.com/
To be sure, log the value of myInfo[0].isTrue before redux update
useEffect(() => {
console.log("myInfo[0].isTrue", myInfo[0].isTrue);
uploadToRedux(myInfo); // this is redux dispatch. I upload to redux "only" here.
}, []);
As I know, you need to dispatch yourself when you request a server (API) to return data to you. So it needs to be async .but when it's not async you just pass the data to store and it will dispatch itself.
So whenever your data comes back after a delay it's an async request and you need dispatch data when you received data. (if you don't you may get an error because redux dispatch an undefined data)
I hope it be helpful to you.

How can we implement componentWillUnmount using react hooks?

The method componentWillUnmount() is invoked immediately before a component is unmounted and destroyed. If we use useEffect with an empty array ([]) as the second argument and put our function in return statement it will be executed after the component is unmounted and even after another component will be mounted. This is done for performance reasons as far as I understand. In order not to delay rendering.
So the question is - how can we call some function using hooks before a component gets unmounted?
What I am trying to do is an application which saves user's input as he types (without submitting form). I use setInterval to save updated text every N seconds. And I need to force save updates before the component will unmount. I don't want to use prompt by react router before navigating. This is an electron application. I appreciate any thoughts or advice on how to implement such functionality.
Update
Unfortunately, Effects with Cleanup run after letting the browser paint. More details can be found here: So What About Cleanup?. It basically means that cleanup is run after a component is unmounted and it is not the same as executing code in componentWillUnmount(). I can clearly see the sequence of calls if I put console.log statements in the cleanup code and in another component. The question is whether we can execute some code before a component is unmounted using hooks.
Update2
As I can see I should better describe my use case. Let's imagine a theoretical app which holds its data in a Redux store. And we have two components with some forms. For simplicity, we don't have any backend or any async logic. We use only Redux store as data storage.
We don't want to update Redux store on every keystroke. So we keep actual values in the local component's state which we initialize with values from the store when a component mounts. We also create an effect which sets up a setInterval for 1s.
We have the following process. A User types something. Updates are stored in the local component state until our setInterval callback is called. The callback just puts data in the store (dispatches action). We put our callback in the useEffect return statement to force save to store when the component gets unmounted because we want to save data to store in this case as soon as possible.
The problem comes when a user types something in the first component and immediately goes to the second component (faster than 1s). Since the cleanup in our first component will be called after re-rendering, our store won't be updated before the second component gets mounted. And because of that, the second component will get outdated values to its local state.
If we put our callback in componentWillUnmount() it will be called before unmounting and the store will be updated before the next component mounts. So can we implement this using hooks?
componentWillUnmount can be simulated by returning a function inside the useEffect hook. The returned function will be called just before every rerendering of the component. Strictly speaking, this is the same thing but you should be able to simulate any behaviour you want using this.
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
})
Update
The above will run every time there is a rerender. However, to simulate the behaviour only on mounting and unmounting (i.e. componentDidMount and componentWillUnmount). useEffect takes a second argument which needs to be an empty array.
useEffect(() => {
const unsubscribe = api.createSubscription()
return () => unsubscribe()
}, [])
See a more detailed explanation of the same question here.
Since the introduction of the useLayoutEffect hook, you can now do
useLayoutEffect(() => () => {
// Your code here.
}, [])
to simulate componentWillUnmount. This runs during unmount, but before the element has actually left the page.
The question here is how do you run code with hooks BEFORE unmount? The return function with hooks runs AFTER unmount and whilst that doesn’t make a difference for most use cases, their are some where it is a critical difference.
Having done a bit of investigation on this, I have come to the conclusion that currently hooks simply does not provide a direct alternative to componentWillUnmount. So if you have a use case that needs it, which is mainly for me at least, the integration of non-React libs, you just have to do it the old way and use a component.
Update: see the answer below about UseLayoutEffect() which looks like it may solve this issue.
I agree with Frank, but the code needs to look like this otherwise it will run only on the first render:
useLayoutEffect(() => {
return () => {
// Your code here.
}
}, [])
This is equivalent to ComponentWillUnmount
Similar to #pritam's answer, but with an abstracted code example. The whole idea of useRef is to allow you to keep track of the changes to the callback and not have a stale closure at the time of execution. Hence, the useEffect at the bottom can have an empty dependency array to ensure it only runs when the component unmounts. See the code demo.
Reusable hook:
type Noop = () => void;
const useComponentWillUnmount = (callback: Noop) => {
const mem = useRef<Noop>();
useEffect(() => {
mem.current = callback;
}, [callback]);
useEffect(() => {
return () => {
const func = mem.current as Noop;
func();
};
}, []);
};
After a bit of research, found that - you could still accomplish this. Bit tricky but should work.
You can make use of useRef and store the props to be used within a closure such as render useEffect return callback method
function Home(props) {
const val = React.useRef();
React.useEffect(
() => {
val.current = props;
},
[props]
);
React.useEffect(() => {
return () => {
console.log(props, val.current);
};
}, []);
return <div>Home</div>;
}
DEMO
However a better way is to pass on the second argument to useEffect so that the cleanup and initialisation happens on any change of desired props
React.useEffect(() => {
return () => {
console.log(props.current);
};
}, [props.current]);
I got in a unique situation where the useEffect(() => () => { ... }, []); answers did not work for me. This is because my component never got rendered — I was throwing an exception before I could register the useEffect hook.
function Component() {
useEffect(() => () => { console.log("Cleanup!"); }, []);
if (promise) throw promise;
if (error) throw error;
return <h1>Got value: {value}</h1>;
}
In the above example, by throwing a Promise<T> that tells react to suspend until the promise is resolved. However, once the promise is resolved, an error is thrown. Since the component never gets rendered and goes straight to an ErrorBoundary, the useEffect() hook is never registered!
If you're in a similar situation as myself, this little code may help:
To solve this, I modified my ErrorBoundary code to run a list of teardowns once it was recovered
export default class ErrorBoundary extends Component {
// ...
recover() {
runTeardowns();
// ...
}
// ...
}
Then, I created a useTeardown hook which would add teardowns that needed to be ran, or make use of useEffect if possible. You'll most likely need to modify it if you have nesting of error boundaries, but for my simple usecase, it worked wonderfully.
import React, { useEffect, useMemo } from "react";
const isDebugMode = import.meta.env.NODE_ENV === "development";
const teardowns: (() => void)[] = [];
export function runTeardowns() {
const wiped = teardowns.splice(0, teardowns.length);
for (const teardown of wiped) {
teardown();
}
}
type Teardown = { registered?: boolean; called?: boolean; pushed?: boolean } & (() => unknown);
/**
* Guarantees a function to run on teardown, even when errors occur.
*
* This is necessary because `useEffect` only runs when the component doesn't throw an error.
* If the component throws an error before anything renders, then `useEffect` won't register a
* cleanup handler to run. This hook **guarantees** that a function is called when the component ends.
*
* This works by telling `ErrorBoundary` that we have a function we would like to call on teardown.
* However, if we register a `useEffect` hook, then we don't tell `ErrorBoundary` that.
*/
export default function useTeardown(onTeardown: () => Teardown, deps: React.DependencyList) {
// We have state we need to maintain about our teardown that we need to persist
// to other layers of the application. To do that, we store state on the callback
// itself - but to do that, we need to guarantee that the callback is stable. We
// achieve this by memoizing the teardown function.
const teardown = useMemo(onTeardown, deps);
// Here, we register a `useEffect` hook to run. This will be the "happy path" for
// our teardown function, as if the component renders, we can let React guarantee
// us for the cleanup function to be ran.
useEffect(() => {
// If the effect gets called, that means we can rely on React to run our cleanup
// handler.
teardown.registered = true;
return () => {
if (isDebugMode) {
// We want to ensure that this impossible state is never reached. When the
// `runTeardowns` function is called, it should only be ran for teardowns
// that have not been able to be hook into `useEffect`.
if (teardown.called) throw new Error("teardown already called, but unregistering in useEffect");
}
teardown();
if (isDebugMode) {
// Because `teardown.registered` will already cover the case where the effect
// handler is in charge of running the teardown, this isn't necessary. However,
// this helps us prevent impossible states.
teardown.called = true;
}
};
}, deps);
// Here, we register the "sad path". If there is an exception immediately thrown,
// then the `useEffect` cleanup handler will never be ran.
//
// We rely on the behavior that our custom `ErrorBoundary` component will always
// be rendered in the event of errors. Thus, we expect that component to call
// `runTeardowns` whenever it deems it appropriate to run our teardowns.
// Because `useTeardown` will get called multiple times, we want to ensure we only
// register the teardown once.
if (!teardown.pushed) {
teardown.pushed = true;
teardowns.push(() => {
const useEffectWillCleanUpTeardown = teardown.registered;
if (!useEffectWillCleanUpTeardown) {
if (isDebugMode) {
// If the useEffect handler was already called, there should be no way to
// re-run this teardown. The only way this impossible state can be reached
// is if a teardown is called multiple times, which should not happen during
// normal execution.
const teardownAlreadyCalled = teardown.called;
if (teardownAlreadyCalled) throw new Error("teardown already called yet running it in runTeardowns");
}
teardown();
if (isDebugMode) {
// Notify that this teardown has been called - useful for ensuring that we
// cannot reach any impossible states.
teardown.called = true;
}
}
});
}
}
It does not matter wether the returned function from useEffect gets called before or after the component unmounted: You still have access to the states valuey through the closure:
const [input, setInput] = useState(() => Store.retrieveInput());
useEffect(() => {
return () => Store.storeInput(input); // < you can access "input" here, even if the component unmounted already
}, []);
If you don't manage the input in the components state, your whole structure is broken and should be changed to manage state at the right place. In your case, you should lift the shared input state of the components to the parent.
ReactJS docs on hooks specify this:
Effects may also optionally specify how to “clean up” after them by
returning a function.
So any function you return in your useEffect hook, will be executed when the component unmounts, as well as before re-running the effect due to a subsequent render.

Categories

Resources