Returning a callback from useEffect - javascript

Sorry for the newbie question:
I'm using useEffect to avoid setting state on an unmounted component, and I was wondering why does this work:
useEffect(() => {
let isMounted = true
actions.getCourseDetails(fullUrl)
.then(data => {
if (isMounted) {
actions.setOwner(data.course.Student.id);
setDetails(data.course);
}
});
return () => {
isMounted = false;
}
}, [actions, fullUrl]);
...but when I return a variable instead of a callback it doesn't work?:
useEffect(() => {
let isMounted = true
actions.getCourseDetails(fullUrl)
.then(data => {
if (isMounted) {
actions.setOwner(data.course.Student.id);
setDetails(data.course);
}
});
isMounted = false;
return isMounted; //returning a variable instead of a callback
}, [actions, fullUrl]);
Thanks!

The syntax of useEffect is to optionally return a dispose function. React will call this dispose function ONLY when one of the dependencies changes or when it unmounts. to "release" stuff that no longer relevant.
For example, you want to wait X seconds after the render, and then change the state:
useEffect(() => {
setTimeout(() => setState('Timeout!', timeToWait));
}, [timeToWait])
Imagen that this component mounts and then after one second unmounts. Without a dispose function the timer will run and React will try to run setState on unmounted component, this will result in an error.
The proper way to do it is to use the dispose function:
useEffect(() => {
const id = setTimeout(() => setState('Timeout!', timeToWait));
return () => clearTimeout(id);
}, [timeToWait])
So every time the timeToWait dependency changes for some reason, the dispose function will stop the timer and the next render will create a new one with the new value. or when the component unmounts.
In your example, the order of execution will be:
Define isMounted and set it to true
Start async action (this will run next tick)
Set isMounted to false
return a variable (Not a function)
So you have 2 problems in your (Not-working) example. you don't return a dispose function, and you change isMounted to false almost immediately after you define it. when the promise will run the isMounted will be false no matter what. If you'd use a dispose function (The working example), only when React will call it the isMounted to turn to false

Related

How to fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function error

Warning: Can't perform a React state update on an unmounted component. 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.
useEffect(() => {
const unsubscribe = streamCourses({
next: (querySnapshot) => {
const task = querySnapshot.docs.map((docSnapshot) =>
mapDocTask(docSnapshot)
);
setCourseDetails(task);
},
error: (error) => console.log(error),
});
return unsubscribe;
}, [setCourseDetails]);
I had a similar issue to this. What I had to do to solve it was two things:
(1) I created a State boolean isMounted which was set to true by default and was used to wrap the contents of my useEffects so that the contents of my useEffects would only run if the screen was mounted.
(2) I created a useEffect dedicated solely to cleanup. Meaning this useEffect had nothing besides a return statement in it which set the various State variables I had to their default values.
Example:
useEffect(() => {
if (isMounted) {
const unsubscribe = streamCourses({
next: (querySnapshot) => {
const task = querySnapshot.docs.map((docSnapshot) =>
mapDocTask(docSnapshot)
);
setCourseDetails(task);
},
error: (error) => console.log(error),
});
return unsubscribe;
}
}, [setCourseDetails]);
useEffect(() => {
return () => {
setCourseDetails(null);
setIsMounted(false);
}
}, []);

how do I set up a setTimeout without using a callback by the useEffect hook?

so as part of learning react I am currently converting a class-based App to a functional one, I've encountered some issues with my code since I can't use the callback function in the following context:
class ColorBox extends Component {
constructor(props) {
super(props);
this.state = { copied: false };
this.changeCopyState = this.changeCopyState.bind(this);
}
changeCopyState() {
this.setState({ copied: true }, () => {
**setTimeout(() => this.setState({ copied: false }), 1500);**
});
}
I've tried to change it using the useEffect hook, to the following:
function ColorBox(props) {
const [isCopied, setIsCopied] = useState(false)
useEffect(() => setTimeout(() => setIsCopied(false), 1500), [isCopied])
const changeCopyState = () => {
setIsCopied(true)
};
but the problem is that the useEffect renders at the first render which makes the app glitch if I don't wait for 1500ms before clicking on the copy button.
Any help would be greatly appreciated!!
effects will fire whenever the values of your dependencies change. However, what you want according to your class-based approach is to, after setting isCopied to true, set it to false after 1500 ms.
To do so, check the current value of isCopied in your effect before firing the timeout.
function ColorBox(props) {
const [isCopied, setIsCopied] = useState(false)
useEffect(() => {
if (isCopied) {
setTimeout(() => setIsCopied(false), 1500)
}
}, [isCopied, setIsCopied])
const changeCopyState = () => {
setIsCopied(true)
};
}
In addition to that, for consistency, you might want to use clearTimeout when unmounting your effect (in order to avoid, for instance, calling setIsCopied after the component has unmounted).
To do so, the effect has to be like this
useEffect(() => {
if (isCopied) {
let timeoutId = setTimeout(() => setIsCopied(false), 1500)
return () => clearTimeout(timeout)
}
}, [isCopied, setIsCopied])
When you don't specify a curly braces {} in arrow function, it will return a value. In useEffect you don't need a value to be returned (the only exception is componentWillUnmount lifecycle method). That drove to unpredictable behavior and timeout was fired at the initial render. Use curly braces {} in your useEffect arrow function instead
useEffect(() => {
setTimeout(() => setIsCopied(false),1500)
}, [isCopied]);

React useEffect executing last function

I need to have 2 different functions that update 2 different components only once in the beginning. Hence, I'm using useEffect. The code is as follows
const loadCategories = () => {
getCategories().then((c) => setValues({ ...values, categories: c.data }));
}
const loadStores = () => {
getStores().then((c) => setValues({ ...values, stores: c.data }));
}
useEffect(() => {
loadStores();
loadCategories();
}, []);
Both the functions are setting the values of the dropdown elements
The problem is though both functions are exectued, only loadCategories() function logic is reflected in the UI. How to make both functions logic reflect in the UI?
first better practice to add those function in useEffect or to wrap them in useCallback hook.
second both or your function are promises so each may not resolve at same time and when you trying to update state values will keep it initial value that why your first function is not reflecting in the ui instead use setState callback to get the previous state like this :
useEffect(() => {
const loadCategories = () => {
getCategories().then((c) => setValues(prevState=>({ ...prevState, categories: c.data })));
}
const loadStores = () => {
getStores().then((c) => setValues(prevState=>({ ...prevState, stores: c.data })));
}
loadStores();
loadCategories();
}, []);
Promise and useEffect can be challenging as the component might dismount before you promise is full-filled.
Here is a solution which works quite well:
useEffect(() => {
let isRunning = true;
Promise.all([
getCategories(),
getStores()
]).then(([stores, categories]) => {
// Stop if component was unmounted:
if (!isRunning) { return }
// Do anything you like with your lazy load values:
console.log(stores, categories)
});
return () => {
isRunning = false;
}
}, []);
Wait for both promises to resolve and then use the data from both to update your state, combined in whatever way you see fit.
useEffect(() => {
Promise.all([getCategories(), getStores()]).then(([categories, stores]) => {
setValues({categories, stores})
});
}, []);
The problem with what you had before (as you experienced) is that values is always the value at the point at which useState was run on this render.
If you really want to do the updates separately, than you can look into useReducer: https://reactjs.org/docs/hooks-reference.html#usereducer
You are lying to React about dependencies. Both your functions depend on the values state variable. That's also why it does not work: When the hooks get run, values gets closured, then when setValues runs values changes (gets set to a new object), however inside the closure it is still referencing the old values.
You can easily resolve that by passing a callback to setValues, this way you do not have a dependency to the outside and the values update is atomic:
setValues(values => ({ ...values, stores: c.data }));

How can i execute a function after changing a react hook?

When I execute setState in class component, I can pass the callback to the last argument, and callback execute after changing State:
this.setState({}, () => { *execute after changing state* })
My example:
const foo = () => {
setOpen(false);
bar(); // this function should be performed after completion setOpen changing, but setOpen is async func
}
Question: How to execute bar () immediately after the update of the hook through setOpen is completed with the false argument?
You'd do it like this:
const [ isOpen, setIsOpen ] = useState( false );
useEffect(() => {
if( !isOpen ) {
bar();
}
}, [ isOpen ]);
The useEffect hook is triggered once a change to isOpen is detected because it is listed in the dependencies for the useEffect hook.
From what I've gathered, you're dealing with some animations upon your component closing and that bar is the function to unmount the component.
Technically what you'd be looking for (if the animation takes 500ms to complete) is this:
// Add this line somewhere outside of your component, preferably in a helpers file.
const delay = ms => new Promise(res => setTimeout(res, ms));
const foo = async () => {
setOpen(false);
await delay(500) // Change this value to match the animation time
bar();
}
This should allow your animation to ru before unmounting the component.

Data fetching with React hooks cleanup the async callback

I was starting to build some of my new components with the new and shiny React Hooks. But I was using a lot of async api calls in my components where I also show a loading spinner while the data is fetching. So as far as I understood the concept this should be correct:
const InsideCompontent = props => {
const [loading, setLoading] = useState(false);
useEffect(() => {
...
fetchData()
...
},[])
function fetchData() {
setFetching(true);
apiCall().then(() => {
setFetching(false)
})
}
}
So this is just my initial idea of how this might work. Just a small example.
But what happens if the parent component has now a condition changed that this component gets unmounted before the async call is finished.
Is there somehow a check where I can check if the component is still mounted before I call the setFetching(false) in the api callback?
Or am I missing something here ?
Here is working example :
https://codesandbox.io/s/1o0pm2j5yq
EDIT:
There was no really issue here. You can try it out here:
https://codesandbox.io/s/1o0pm2j5yq
The error was from something else, so with hooks you don't need to check if the component is mounted or not before doing a state change.
Another reason why to use it :)
You can use the useRef hook to store any mutable value you like, so you could use this to toggle a variable isMounted to false when the component is unmounted, and check if this variable is true before you try to update the state.
Example
const { useState, useRef, useEffect } = React;
function apiCall() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Foo");
}, 2000);
});
}
const InsideCompontent = props => {
const [state, setState] = useState({ isLoading: true, data: null });
const isMounted = useRef(true);
useEffect(() => {
apiCall().then(data => {
if (isMounted.current) {
setState({ isLoading: false, data });
}
});
return () => {
isMounted.current = false
};
}, []);
if (state.isLoading) return <div>Loading...</div>
return <div>{state.data}</div>;
};
function App() {
const [isMounted, setIsMounted] = useState(true);
useEffect(() => {
setTimeout(() => {
setIsMounted(false);
}, 1000);
}, []);
return isMounted ? <InsideCompontent /> : null;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Here's a Hook for fetching data that we use internally. It also allows manipulating the data once it's fetched and will throw out data if another call is made prior to a call finishing.
https://www.npmjs.com/package/use-data-hook
(You can also just include the code if you don't want an entire package)
^ Also this converts to JavaScript by simply removing the types.
It is loosely inspired by this article, but with more capabilities, so if you don't need the data-manipulation you can always use the solution in that article.
Assuming that this is the error you've encountered:
Warning: Can't perform a React state update on an unmounted component. 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.
React complains and hints you at the same time. If component has to be unmounted but there is an outstanding network request, it should be cancelled. Returning a function from within useEffect is a mechanism for performing any sort of cleanup required (docs).
Building on your example with setTimeout:
const [fetching, setFetching] = useState(true);
useEffect(() => {
const timerId = setTimeout(() => {
setFetching(false);
}, 4000);
return () => clearTimeout(timerId)
})
In case component unmounts before the callback fires, timer is cleared and setFetching won't be invoked.

Categories