I have a group of react-native components that have similar interfaces. The important method in the interface is the run method. Users can execute this run method by pressing an action button for each of the components or by pressing an action button in the parent level, that will iterate through each of these similar components using their ref and call the run method. Here is a simplified version of the setup.
const Test1 = forwardRef((props, ref) => {
const [importantState, setimportantState] = useState('initial')
useImperativeHandle(ref, () => ({
run: run,
}))
const run = async () => {
// This function will open a modal
// where users will eventually change the important state
openInteractionModal()
// The idea of this promiss is to settle when the users
// eventually change the importantState thorugh thier interaction
// It does so by checking the state every 100 miliseconds
// This however is a problem as a result of the setInterval function will
// inclose the initial state instead of getting the new value each time
return new Promise((resolve, reject) => {
let interval = setInterval(() => {
if (importantStat === 'complete') {
resolve('success')
clearInterval(interval)
} else if (importantStat === 'failed') {
reject('failed')
clearInterval(interval)
}
}, 100)
})
}
return (
<>
<Button title="run" onPress={run} />
</>
)
})
const parentComponent = () => {
const test1Ref = userRef(null)
const test2Ref = userRef(null)
const test3Ref = userRef(null)
const testRefs = [test1Ref, test2Ref, test3Ref]
const autoRun = async () => {
testRefs.forEach(ref => {
await ref?.current.run()
});
}
return (
<>
<Button title="Auto run" onPress={autoRun} />
<Test1 ref={test1Ref} />,
<Test2 ref={test2Ref} />,
<Test3 ref={test3Ref} />,
</>
)
}
The problem I am having right now is the promise I am returning from these individual components never settles as a result of the closure I mentioned above in the comments. Is there a way to overcome this closure, or is there a better react-native pattern that I am using right now?
Don't go polling. Instead, pass the resolve function as an argument to openInteractionModal, and call it together with setImportantState from the modal dialog.
If that is for some reason not possible, you can use a ref to store the resolve function and run it from an effect when the state changes:
const onImportantStateChange = useRef();
useEffect(() => {
if (!onImportantStateChange.current) return;
if (importantState === 'complete') {
onImportantStateChange.current('success');
onImportantStateChange.current = undefined;
} else if (importantState === 'failed') {
onImportantStateChange.current(Promise.reject(new Error('failure')));
onImportantStateChange.current = undefined;
}
}, [importantState])
const run = async () => {
return new Promise(resolve => {
onImportantStateChange.current = resolve;
// This function will open a modal
// where users will eventually change the important state
openInteractionModal();
});
}
Create a ref and store the status in that instead of the state of the Test1 component.
Like that
const importantState = useRef(null);
Set value in the current of the ref
importantState.current="complete"
Later use it in the setInterval like
if(importantState.current === "complete")
I have rewritten a Child class component in React to a functional component. Here is the simplified code example.
For sure, as so often, this is a simplified code and more things are done with the value in the parent component. That's why we have and need it there.
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue) => {
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
}
return (
<Child value={value} onChange={handleChange}/>
);
}
const Child = (props) => {
const {value} = props;
// This solution does not work for me,
// because it's always triggered, when
// `value` changes. I only want to trigger
// `logValueFromProp` after clicking the
// Button.
useEffect(() => {
logValueFromProp();
}, [value]);
const handleClick = () => {
// some calculations to get `newValue`
// are happening here
props.onChange(newValue);
logValueFromProp();
}
const logValueFromProp = () {
console.log(prop.value);
}
return (
<Button onClick={handleClick} />
);
}
What I want to do is to log a properties value, but only if it got changed by clicking the button. So just using a useEffect does not work for me.
Before changing the child component to a functional component, the property had its new value before I was calling logValueFromProp(). Afterwards it doesn't. I guess that's cause of some timing, and I was just lucky that the property was updated before the function was called.
So the question is: How would you solve this situation? One solution I thought of was a state in the child component which I set when the button is clicked and in the useEffect I only call the function when the state is set and then reset the state. But that doesn't feel like the optimal solution to me...
Three possible solutions for you
Pass logValueFromProp the value directly — but in a comment you've explained that the value might be modified slightly by the parent component before being set on the child, which would make this not applicable.
Use a flag in a ref. But if the parent doesn't always change the prop, that would be unreliable.
Have the parent accept a callback in its handleChange.
#1
If possible, I'd pass the value directly to logValueFromProp when you want to log it. That's the simple, direct solution:
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue);
logValueFromProp(newValue);
};
const logValueFromProp = (newValue = prop.value) {
console.log(newValue);
};
return (
<Button onClick={handleClick} />
);
};
But in a comment you've said the new value may not be exactly the same as what you called props.onChange with.
#2
You could use a ref to remember whether you want to log it when the component function is next called (which will presumably be after it changes):
const Child = (props) => {
const {value} = props;
const logValueRef = useRef(false);
if (logValueRef.current) {
logValueFromProp();
logValueRef.current = false;
}
const handleClick = () => {
props.onChange(newValue);
logValueRef.current = true;
};
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
Using a ref instead of a state member means that when you clear the flag, it doesn't cause a re-render. (Your component function is only called after handleClick because the parent changes the value prop.)
Beware that if the parent component doesn't change the value when you call prop.onChange, the ref flag will remain set and then your component will mistakenly log the next changed value even if it isn't from the button. For that reason, it might make sense to try to move the logging to the parent, which knows how it responds to onChange.
#3
Given the issues with both of the above, the most robust solution would seem to be to modify Parent's handleChange so that it calls a callback with the possibly-modified value:
const Parent = (props) => {
const [value, setValue] = useState(null);
const handleChange = (newValue, callback) => {
// ^^^^^^^^^^−−−−−−−−−−−−−−−−− ***
// do something with newValue and probably change it
// store the result in `newChangedValue`
setValue(newChangedValue);
if (callback) { // ***
callback(newChangedValue); // ***
} // ***
};
return (
<Child value={value} onChange={handleChange}/>
);
};
const Child = (props) => {
const {value} = props;
const handleClick = () => {
props.onChange(newValue, logValueFromProp);
// ^^^^^^^^^^^^^^^^^^−−−−−−−−−−−−−− ***
}
const logValueFromProp = () {
console.log(prop.value);
};
return (
<Button onClick={handleClick} />
);
};
This answer is based upon the answer of T.J. Crowder (#2).
You can create a custom hook that accepts a callback and dependencies. And returns a function that will trigger a re-render (by using useState instead of useContext) calling the callback in the process.
I've enhanced his answer by allowing you to pass a dependency array which will be used to determine if the callback is called. If the dependency array is omitted, the callback is always called. When passed, the callback is only called if there was a change in the dependency array.
I went for the name useTrigger in the example below, but depending on preference you might like another name better. For example useChange.
const { useState, useCallback } = React;
const useTrigger = (function () {
function zip(a1, a2) {
return a1.map((_, i) => [a1[i], a2[i]]);
}
// compares 2 arrays assuming the length is the same
function equals(a1, a2) {
return zip(a1, a2).every(([e1, e2]) => Object.is(e1, e2));
}
return function (callback, deps) {
const [trigger, setTrigger] = useState(null);
if (trigger) {
if (!deps || !equals(deps, trigger.deps)) {
callback(...trigger.args);
}
setTrigger(null);
}
return useCallback((...args) => {
setTrigger({ args, deps });
}, deps);
}
})();
function Parent() {
const [value, setValue] = useState(null);
function handleChange(newValue) {
// Sometimes the value is changed, triggering `logValueFromProp()`.
// Sometimes it isn't.
if (Math.random() < 0.66) newValue++;
setValue(newValue);
}
return <Child value={value} onChange={handleChange} />;
}
function Child({ value, onChange }) {
const logValueFromProp = useTrigger(() => {
console.log(value);
}, [value]);
function handleClick() {
onChange(value || 0);
logValueFromProp();
}
return (
<button onClick={handleClick}>
Click Me!
</button>
);
}
ReactDOM.render(<Parent />, document.querySelector("#demo"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="demo"></div>
My UI was working fine until it was using a class component. Now I am refactoring it to a functional component.
I have to load my UI based on the data I receive from an API handler. My UI will reflect the state of the camera which is present inside a room. Every time the camera is turned on or off from the room, I should receive the new state from the API apiToGetCameraState.
I want the console.log present inside the registerVideoStateUpdateHandlerWrapper to print both on UI load for the first time and also to load every time the video state is changed in the room. However, it doesn't work when the UI is loaded for the first time.
This is how my component looks like:
const Home: React.FunctionComponent<{}> = React.memo(() => {
const [video, setToggleVideo] = React.useState(true);
const registerVideoStateUpdateHandlerWrapper = React.useCallback(() => {
apiToGetCameraState(
(videoState: boolean) => {
// this log does not show up when the UI is loaded for the first time
console.log(
`Video value before updating the state: ${video} and new state is: ${videoState} `
);
setToggleVideo(videoState);
}
);
}, [video]);
React.useEffect(() => {
//this is getting called when the app loads
alert(`Inside use effect for Home component`);
registerVideoStateUpdateHandlerWrapper ();
}, [registerVideoStateUpdateHandlerWrapper ]);
return (
<Grid>
<Camera
isVideoOn={video}
/>
</Grid>
);
});
This was working fine when my code was in class component. This is how the class component looked like.
class Home extends Component {
registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
console.log(`ToggleVideo value before updating the state: ${this.state.toggleCamera} and new state is: ${videoState}`);
this.setStateWrapper(videoState.toString());
})
}
setStateWrapper = (toggleCameraUpdated) => {
console.log("Inside setStateWrapper with toggleCameraUpdated:" + toggleCameraUpdated);
this.setState({
toggleCamera: (toggleCameraUpdated === "true" ) ? "on" : "off",
});
}
constructor(props) {
super(props);
this.state = {
toggleCamera: false,
};
}
componentDidMount() {
console.log(`Inside componentDidMount with toggleCamera: ${this.state.toggleCamera}`)
this.registerVideoStateUpdateHandlerWrapper ();
}
render() {
return (
<div>
<Grid>
<Camera isVideoOn={this.state.toggleCamera} />
</Grid>
);
}
}
What all did I try?
I tried removing the useCallback in the registerVideoStateUpdateHandlerWrapper function and also the dependency array from React.useEffect and registerVideoStateUpdateHandlerWrapper. It behaved the same
I tried updating the React.useEffect to have the code of registerVideoStateUpdateHandlerWrapper in it but still no success.
Move registerVideoStateUpdateHandlerWrapper() inside the useEffect() callback like this. If you want to log the previous state when the state changes, you should use a functional update to avoid capturing the previous state through the closure:
const Home = () => {
const [video, setVideo] = useState(false);
useEffect(() => {
console.log('Inside useEffect (componentDidMount)');
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo((prevVideo) => {
console.log(`Video value before updating the state: ${prevVideo} and new state is: ${videoState}`);
return videoState;
});
});
};
registerVideoStateUpdateHandlerWrapper();
}, []);
return (
<Grid>
<Camera isVideoOn={video} />
</Grid>
);
};
When you no longer actually need to log the previous state, you should simplify registerVideoStateUpdateHandlerWrapper() to:
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo(videoState);
});
};
import React from 'react'
const Home = () => {
const [video, setVideo] = useState(null);
//default video is null, when first load video will change to boolean, when the Camera component will rerender
const registerVideoStateUpdateHandlerWrapper = () => {
apiToGetCameraState((videoState) => {
setVideo(videoState);
});
};
useEffect(() => {
registerVideoStateUpdateHandlerWrapper();
}, []);
return (
<Grid>
<Camera isVideoOn={video} />
</Grid>
);
};
export default Home
componentDidMount() === useEffect()
'useEffect' => import from 'react'
// componentDidMount()
useEffect(() => {
// Implement your code here
}, [])
// componentDidUpdate()
useEffect(() => {
// Implement your code here
}, [ update based on the props, state in here if you mention ])
e.g:
const [loggedIn, setLoggedIn] = useState(false);
useEffect(() => {
// Implement the code here
}, [ loggedIn ]);
the above code will act as equivalent to the componentDidUpdate based on 'loggedIn' state
When fetching data I'm getting: Can't perform a React state update on an unmounted component. The app still works, but react is suggesting I might be causing a memory leak.
This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function."
Why do I keep getting this warning?
I tried researching these solutions:
https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
but this still was giving me the warning.
const ArtistProfile = props => {
const [artistData, setArtistData] = useState(null)
const token = props.spotifyAPI.user_token
const fetchData = () => {
const id = window.location.pathname.split("/").pop()
console.log(id)
props.spotifyAPI.getArtistProfile(id, ["album"], "US", 10)
.then(data => {setArtistData(data)})
}
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [])
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
}
Edit:
In my api file I added an AbortController() and used a signal so I can cancel a request.
export function spotifyAPI() {
const controller = new AbortController()
const signal = controller.signal
// code ...
this.getArtist = (id) => {
return (
fetch(
`https://api.spotify.com/v1/artists/${id}`, {
headers: {"Authorization": "Bearer " + this.user_token}
}, {signal})
.then(response => {
return checkServerStat(response.status, response.json())
})
)
}
// code ...
// this is my cancel method
this.cancelRequest = () => controller.abort()
}
My spotify.getArtistProfile() looks like this
this.getArtistProfile = (id,includeGroups,market,limit,offset) => {
return Promise.all([
this.getArtist(id),
this.getArtistAlbums(id,includeGroups,market,limit,offset),
this.getArtistTopTracks(id,market)
])
.then(response => {
return ({
artist: response[0],
artistAlbums: response[1],
artistTopTracks: response[2]
})
})
}
but because my signal is used for individual api calls that are resolved in a Promise.all I can't abort() that promise so I will always be setting the state.
For me, clean the state in the unmount of the component helped.
const [state, setState] = useState({});
useEffect(() => {
myFunction();
return () => {
setState({}); // This worked for me
};
}, []);
const myFunction = () => {
setState({
name: 'Jhon',
surname: 'Doe',
})
}
Sharing the AbortController between the fetch() requests is the right approach.
When any of the Promises are aborted, Promise.all() will reject with AbortError:
function Component(props) {
const [fetched, setFetched] = React.useState(false);
React.useEffect(() => {
const ac = new AbortController();
Promise.all([
fetch('http://placekitten.com/1000/1000', {signal: ac.signal}),
fetch('http://placekitten.com/2000/2000', {signal: ac.signal})
]).then(() => setFetched(true))
.catch(ex => console.error(ex));
return () => ac.abort(); // Abort both fetches on unmount
}, []);
return fetched;
}
const main = document.querySelector('main');
ReactDOM.render(React.createElement(Component), main);
setTimeout(() => ReactDOM.unmountComponentAtNode(main), 1); // Unmount after 1ms
<script src="//cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.development.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.development.js"></script>
<main></main>
For example, you have some component that does some asynchronous actions, then writes the result to state and displays the state content on a page:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Let's say that user clicks some link when doVeryLongRequest() still executes. MyComponent is unmounted but the request is still alive and when it gets a response it tries to set state in lines (1) and (2) and tries to change the appropriate nodes in HTML. We'll get an error from subject.
We can fix it by checking whether compponent is still mounted or not. Let's create a componentMounted ref (line (3) below) and set it true. When component is unmounted we'll set it to false (line (4) below). And let's check the componentMounted variable every time we try to set state (line (5) below).
The code with fixes:
export default function MyComponent() {
const [loading, setLoading] = useState(false);
const [someData, setSomeData] = useState({});
const componentMounted = useRef(true); // (3) component is mounted
// ...
useEffect( () => {
(async () => {
setLoading(true);
someResponse = await doVeryLongRequest(); // it takes some time
// When request is finished:
if (componentMounted.current){ // (5) is component still mounted?
setSomeData(someResponse.data); // (1) write data to state
setLoading(false); // (2) write some value to state
}
return () => { // This code runs when component is unmounted
componentMounted.current = false; // (4) set it to false when we leave the page
}
})();
}, []);
return (
<div className={loading ? "loading" : ""}>
{someData}
<Link to="SOME_LOCAL_LINK">Go away from here!</Link>
</div>
);
}
Why do I keep getting this warning?
The intention of this warning is to help you prevent memory leaks in your application. If the component updates it's state after it has been unmounted from the DOM, this is an indication that there could be a memory leak, but it is an indication with a lot of false positives.
How do I know if I have a memory leak?
You have a memory leak if an object that lives longer than your component holds a reference to it, either directly or indirectly. This usually happens when you subscribe to events or changes of some kind without unsubscribing when your component unmounts from the DOM.
It typically looks like this:
useEffect(() => {
function handleChange() {
setState(store.getState())
}
// "store" lives longer than the component,
// and will hold a reference to the handleChange function.
// Preventing the component to be garbage collected after
// unmount.
store.subscribe(handleChange)
// Uncomment the line below to avoid memory leak in your component
// return () => store.unsubscribe(handleChange)
}, [])
Where store is an object that lives further up the React tree (possibly in a context provider), or in global/module scope. Another example is subscribing to events:
useEffect(() => {
function handleScroll() {
setState(window.scrollY)
}
// document is an object in global scope, and will hold a reference
// to the handleScroll function, preventing garbage collection
document.addEventListener('scroll', handleScroll)
// Uncomment the line below to avoid memory leak in your component
// return () => document.removeEventListener(handleScroll)
}, [])
Another example worth remembering is the web API setInterval, which can also cause memory leak if you forget to call clearInterval when unmounting.
But that is not what I am doing, why should I care about this warning?
React's strategy to warn whenever state updates happen after your component has unmounted creates a lot of false positives. The most common I've seen is by setting state after an asynchronous network request:
async function handleSubmit() {
setPending(true)
await post('/someapi') // component might unmount while we're waiting
setPending(false)
}
You could technically argue that this also is a memory leak, since the component isn't released immediately after it is no longer needed. If your "post" takes a long time to complete, then it will take a long time to for the memory to be released. However, this is not something you should worry about, because it will be garbage collected eventually. In these cases, you could simply ignore the warning.
But it is so annoying to see the warning, how do I remove it?
There are a lot of blogs and answers on stackoverflow suggesting to keep track of the mounted state of your component and wrap your state updates in an if-statement:
let isMountedRef = useRef(false)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
async function handleSubmit() {
setPending(true)
await post('/someapi')
if (!isMountedRef.current) {
setPending(false)
}
}
This is not an recommended approach! Not only does it make the code less readable and adds runtime overhead, but it might also might not work well with future features of React. It also does nothing at all about the "memory leak", the component will still live just as long as without that extra code.
The recommended way to deal with this is to either cancel the asynchronous function (with for instance the AbortController API), or to ignore it.
In fact, React dev team recognises the fact that avoiding false positives is too difficult, and has removed the warning in v18 of React.
You can try this set a state like this and check if your component mounted or not. This way you are sure that if your component is unmounted you are not trying to fetch something.
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
return () => setDidMount(false);
}, [])
if(!didMount) {
return null;
}
return (
<ArtistProfileContainer>
<AlbumContainer>
{artistData ? artistData.artistAlbums.items.map(album => {
return (
<AlbumTag
image={album.images[0].url}
name={album.name}
artists={album.artists}
key={album.id}
/>
)
})
: null}
</AlbumContainer>
</ArtistProfileContainer>
)
Hope this will help you.
I had a similar issue with a scroll to top and #CalosVallejo answer solved it :) Thank you so much!!
const ScrollToTop = () => {
const [showScroll, setShowScroll] = useState();
//------------------ solution
useEffect(() => {
checkScrollTop();
return () => {
setShowScroll({}); // This worked for me
};
}, []);
//----------------- solution
const checkScrollTop = () => {
setShowScroll(true);
};
const scrollTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
window.addEventListener("scroll", checkScrollTop);
return (
<React.Fragment>
<div className="back-to-top">
<h1
className="scrollTop"
onClick={scrollTop}
style={{ display: showScroll }}
>
{" "}
Back to top <span>⟶ </span>
</h1>
</div>
</React.Fragment>
);
};
I have getting same warning, This solution Worked for me ->
useEffect(() => {
const unsubscribe = fetchData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
if you have more then one fetch function then
const getData = () => {
fetch1();
fetch2();
fetch3();
}
useEffect(() => {
const unsubscribe = getData(); //subscribe
return unsubscribe; //unsubscribe
}, []);
This error occurs when u perform state update on current component after navigating to other component:
for example
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
dispatch(login(res.data.data)); // line#5 logging user in
setSigningIn(false); // line#6 updating some state
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
In above case on line#5 I'm dispatching login action which in return navigates user to the dashboard and hence login screen now gets unmounted.
Now when React Native reaches as line#6 and see there is state being updated, it yells out loud that how do I do this, the login component is there no more.
Solution:
axios
.post(API.BASE_URI + API.LOGIN, { email: username, password: password })
.then((res) => {
if (res.status === 200) {
setSigningIn(false); // line#6 updating some state -- moved this line up
dispatch(login(res.data.data)); // line#5 logging user in
} else {
setSigningIn(false);
ToastAndroid.show(
"Email or Password is not correct!",
ToastAndroid.LONG
);
}
})
Just move react state update above, move line 6 up the line 5.
Now state is being updated before navigating the user away. WIN WIN
there are many answers but I thought I could demonstrate more simply how the abort works (at least how it fixed the issue for me):
useEffect(() => {
// get abortion variables
let abortController = new AbortController();
let aborted = abortController.signal.aborted; // true || false
async function fetchResults() {
let response = await fetch(`[WEBSITE LINK]`);
let data = await response.json();
aborted = abortController.signal.aborted; // before 'if' statement check again if aborted
if (aborted === false) {
// All your 'set states' inside this kind of 'if' statement
setState(data);
}
}
fetchResults();
return () => {
abortController.abort();
};
}, [])
Other Methods:
https://medium.com/wesionary-team/how-to-fix-memory-leak-issue-in-react-js-using-hook-a5ecbf9becf8
If the user navigates away, or something else causes the component to get destroyed before the async call comes back and tries to setState on it, it will cause the error. It's generally harmless if it is, indeed, a late-finish async call. There's a couple of ways to silence the error.
If you're implementing a hook like useAsync you can declare your useStates with let instead of const, and, in the destructor returned by useEffect, set the setState function(s) to a no-op function.
export function useAsync<T, F extends IUseAsyncGettor<T>>(gettor: F, ...rest: Parameters<F>): IUseAsync<T> {
let [parameters, setParameters] = useState(rest);
if (parameters !== rest && parameters.some((_, i) => parameters[i] !== rest[i]))
setParameters(rest);
const refresh: () => void = useCallback(() => {
const promise: Promise<T | void> = gettor
.apply(null, parameters)
.then(value => setTuple([value, { isLoading: false, promise, refresh, error: undefined }]))
.catch(error => setTuple([undefined, { isLoading: false, promise, refresh, error }]));
setTuple([undefined, { isLoading: true, promise, refresh, error: undefined }]);
return promise;
}, [gettor, parameters]);
useEffect(() => {
refresh();
// and for when async finishes after user navs away //////////
return () => { setTuple = setParameters = (() => undefined) }
}, [refresh]);
let [tuple, setTuple] = useState<IUseAsync<T>>([undefined, { isLoading: true, refresh, promise: Promise.resolve() }]);
return tuple;
}
That won't work well in a component, though. There, you can wrap useState in a function which tracks mounted/unmounted, and wraps the returned setState function with the if-check.
export const MyComponent = () => {
const [numPendingPromises, setNumPendingPromises] = useUnlessUnmounted(useState(0));
// ..etc.
// imported from elsewhere ////
export function useUnlessUnmounted<T>(useStateTuple: [val: T, setVal: Dispatch<SetStateAction<T>>]): [T, Dispatch<SetStateAction<T>>] {
const [val, setVal] = useStateTuple;
const [isMounted, setIsMounted] = useState(true);
useEffect(() => () => setIsMounted(false), []);
return [val, newVal => (isMounted ? setVal(newVal) : () => void 0)];
}
You could then create a useStateAsync hook to streamline a bit.
export function useStateAsync<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
return useUnlessUnmounted(useState(initialState));
}
Try to add the dependencies in useEffect:
useEffect(() => {
fetchData()
return () => { props.spotifyAPI.cancelRequest() }
}, [fetchData, props.spotifyAPI])
Usually this problem occurs when you showing the component conditionally, for example:
showModal && <Modal onClose={toggleModal}/>
You can try to do some little tricks in the Modal onClose function, like
setTimeout(onClose, 0)
This works for me :')
const [state, setState] = useState({});
useEffect( async ()=>{
let data= await props.data; // data from API too
setState(users);
},[props.data]);
I had this problem in React Native iOS and fixed it by moving my setState call into a catch. See below:
Bad code (caused the error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
}
setLoading(false) // this line was OUTSIDE the catch call and triggered an error!
}
Good code (no error):
const signupHandler = async (email, password) => {
setLoading(true)
try {
const token = await createUser(email, password)
authContext.authenticate(token)
} catch (error) {
Alert.alert('Error', 'Could not create user.')
setLoading(false) // moving this line INTO the catch call resolved the error!
}
}
Similar problem with my app, I use a useEffect to fetch some data, and then update a state with that:
useEffect(() => {
const fetchUser = async() => {
const {
data: {
queryUser
},
} = await authFetch.get(`/auth/getUser?userId=${createdBy}`);
setBlogUser(queryUser);
};
fetchUser();
return () => {
setBlogUser(null);
};
}, [_id]);
This improves upon Carlos Vallejo's answer.
useEffect(() => {
let abortController = new AbortController();
// your async action is here
return () => {
abortController.abort();
}
}, []);
in the above code, I've used AbortController to unsubscribe the effect. When the a sync action is completed, then I abort the controller and unsubscribe the effect.
it work for me ....
The easy way
let fetchingFunction= async()=>{
// fetching
}
React.useEffect(() => {
fetchingFunction();
return () => {
fetchingFunction= null
}
}, [])
options={{
filterType: "checkbox"
,
textLabels: {
body: {
noMatch: isLoading ?
:
'Sorry, there is no matching data to display',
},
},
}}