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

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.

Related

Cannot retrieve current state inside async function in React.js

I have created some state at the top level component (App), however it seems that when this state is updated, the updated state is not read by the asynchronous function defined in useEffect() (it still uses the previous value), more detail below:
I am attempting to retrieve the state of the const processing in the async function toggleProcessing defined in useEffect(), so that when processing becomes false, the async function exits from the while loop. However, it seems that when the processing updates to false, the while loop still keeps executing.
The behaviour should be as follows: Pressing the 'Begin Processing' button should console log "Processing..." every two seconds, and when that same button is pressed again (now labeled 'Stop Processing'), then "Stopping Processing" should be console logged. However, in practice, "Stopping Processing" is never console logged, and "Processing" is continuously logged forever.
Below is the code:
import React, { useState, useEffect} from 'react'
const App = () => {
const [processing, setProcessing] = useState(false)
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
useEffect(() => {
const toggleProcessing = async () => {
while (processing) {
console.log('Processing...')
await sleep(2000);
}
console.log('Stopping Processing')
}
if (processing) {
toggleProcessing() // async function
}
}, [processing])
return (
<>
<button onClick={() => setProcessing(current => !current)}>{processing ? 'Stop Processing' : 'Begin Processing'}</button>
</>
)
}
export default App;
It really just comes down to being able to read the updated state of processing in the async function, but I have not figure out a way to do this, despite reading similar posts.
Thank you in advance!
If you wish to access a state when using timeouts, it's best to keep a reference to that variable. You can achieve this using the useRef hook. Simply add a ref with the processing value and remember to update it.
const [processing, setProcessing] = useState<boolean>(false);
const processingRef = useRef(null);
useEffect(() => {
processingRef.current = processing;
}, [processing]);
Here is the working code:
import React, { useState, useEffect, useRef} from 'react'
const App = () => {
const [processing, setProcessing] = useState(false)
const processingRef = useRef(null);
const sleep = (ms) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
useEffect(() => {
const toggleProcessing = async () => {
while (processingRef.current) {
console.log('Processing')
await sleep(2000);
}
console.log('Stopping Processing')
}
processingRef.current = processing;
if (processing) {
toggleProcessing() // async function
}
}, [processing])
return (
<>
<button onClick={() => setProcessing(current => !current)}>{processing ? 'Stop Processing' : 'Begin Processing'}</button>
</>
)
}
export default App;
I was interested in how this works and exactly what your final solution was based on the accepted answer. I threw together a solution based on Dan Abramov's useInterval hook and figured this along with a link to some related resources might be useful to others.
I'm curious, is there any specific reason you decided to use setTimeout and introduce async/await and while loop rather than use setInterval? I wonder the implications. Will you handle clearTimeout on clean up in the effect if a timer is still running on unmount? What did your final solution look like?
Demo/Solution with useInterval
https://codesandbox.io/s/useinterval-example-processing-ik61ho
import React, { useState, useEffect, useRef } from "react";
const App = () => {
const [processing, setProcessing] = useState(false);
useInterval(() => console.log("processing"), 2000, processing);
return (
<div>
<button onClick={() => setProcessing((prev) => !prev)}>
{processing ? "Stop Processing" : "Begin Processing"}
</button>
</div>
);
};
function useInterval(callback, delay, processing) {
const callbackRef = useRef();
// Remember the latest callback.
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
callbackRef.current();
}
if (delay !== null && processing) {
let id = setInterval(tick, delay);
console.log(`begin processing and timer with ID ${id} running...`);
// Clear timer on clean up.
return () => {
console.log(`clearing timer with ID ${id}`);
console.log("stopped");
clearInterval(id);
};
}
}, [delay, processing]);
}
export default App;
Relevant Links
Dan Abramov - Making setInterval Declarative with React Hooks
SO Question: React hooks - right way to clear timeouts and intervals
setTimeout and clearTimeout in React with Hooks (avoiding memory leaks by clearing timers on clean up in effects)

Functional component with React.memo() still rerenders

I have a button component that has a button inside that has a state passed to it isActive and a click function. When the button is clicked, the isActive flag will change and depending on that, the app will fetch some data. The button's parent component does not rerender. I have searched on how to force stop rerendering for a component and found that React.memo(YourComponent) must do the job but still does not work in my case. It also make sense to pass a check function for the memo function whether to rerender or not which I would set to false all the time but I cannot pass another argument to the function. Help.
button.tsx
interface Props {
isActive: boolean;
onClick: () => void;
}
const StatsButton: React.FC<Props> = ({ isActive, onClick }) => {
useEffect(() => {
console.log('RERENDER');
}, []);
return (
<S.Button onClick={onClick} isActive={isActive}>
{isActive ? 'Daily stats' : 'All time stats'}
</S.Button>
);
};
export default React.memo(StatsButton);
parent.tsx
const DashboardPage: React.FC = () => {
const {
fetchDailyData,
fetchAllTimeData,
} = useDashboard();
useEffect(() => {
fetchCountry();
fetchAllTimeData();
// eslint-disable-next-line
}, []);
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});
return (
<S.Container>
<S.Header>
<StatsButton
onClick={handleClick}
isActive={statsButtonActive}
/>
</S.Header>
</S.Container>
)
}
fetch functions are using useCallback
export const useDashboard = (): Readonly<DashboardOperators> => {
const dispatch: any = useDispatch();
const fetchAllTimeData = useCallback(() => {
return dispatch(fetchAllTimeDataAction());
}, [dispatch]);
const fetchDailyData = useCallback(() => {
return dispatch(fetchDailyDataAction());
}, [dispatch]);
return {
fetchAllTimeData,
fetchDailyData,
} as const;
};
You haven't posted all of parent.tsx, but I assume that handleClick is created within the body of the parent component. Because the identity of the function will be different on each rendering of the parent, that causes useMemo to see the props as having changed, so it will be re-rendered.
Depending on if what's referenced in that function is static, you may be able to use useCallback to pass the same function reference to the component on each render.
Note that there is an RFC for something even better than useCallback; if useCallback doesn't work for you look at how useEvent is defined for an idea of how to make a better static function reference. It looks like that was even published as a new use-event-callback package.
Update:
It sounds like useCallback won't work for you, presumably because the referenced variables used by the callback change on each render, causing useCallback to return different values, thus making the prop different and busting the cache used by useMemo. Try that useEventCallback approach. Just to illustrate how it all works, here's a naive implementation.
function useEventCallback(fn) {
const realFn = useRef(fn);
useEffect(() => {
realFn.current = fn;
}, [fn]);
return useMemo((...args) => {
realFn.current(...args)
}, []);
}
This useEventCallback always returns the same memoized function, so you'll pass the same value to your props and not cause a re-render. However, when the function is called it calls the version of the function passed into useEventCallback instead. You'd use it like this in your parent component:
const handleClick = useEventCallback(() => {
if (!statsButtonActive) {
fetchDailyData();
} else {
fetchAllTimeData();
}
setStatsButtonActive(!statsButtonActive);
});

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]);

Returning a callback from useEffect

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

Why is the state not being properly updated in this React Native component?

I have a React Native component being used on multiple screens in which I use a reoccurring setTimeout function to animate a carousel of images. The carousel works great, but I want to properly clear out the timer when the screen is navigated away from in the callback function returned from the useEffect hook. (If I don't clear out the timer, then I get a nasty error, and I know I'm supposed to clean up timers anyway.)
For whatever reason though, the state variable I'm trying to set the setTimeout returned timeout ID to seems to be set to null in the callback returned from useEffect.
Here's a simplified version of my code:
const Carousel = () => {
const [timeoutId, setTimeoutId] = useState(null);
const startCarouselCycle = () => {
const newTimeoutId = setTimeout(() => {
// Code here that calls scrollToIndex for the FlatList.
}, 5000);
setTimeoutId(newTimeoutId);
};
const startNextCarouselCycle = () => {
// Other code here.
startCarouselCycle();
};
useEffect(() => {
startCarouselCycle();
return () => {
// This is called when the screen with the carousel
// is navigated away from, but timeoutId is null.
// Why?!
clearTimeout(timeoutId);
};
}, []);
return (
<FlatList
// Non-essential code removed.
horizontal={true}
scrollEnabled={false}
onMomentumScrollEnd={startNextCarouselCycle}
/>
);
};
export default Carousel;
Does anyone have any idea why the state would not be properly updating for use in the returned useEffect callback? Thank you.
You have to remove the dependency array from your useEffect hook like this:
useEffect(() => {
startCarouselCycle();
return () => {
// This is called when the screen with the carousel
// is navigated away from, but timeoutId is null.
// Why?!
clearTimeout(timeoutId);
};
});
This is because your effect is triggered once when the component mounts and it only gets the initial value of your timeoutId.
Based on what I've seen I don't think it's necessary to store your timeout ID in state. Try this:
import React, { useState, useEffect } from 'react';
import { FlatList } from 'react-native';
const Carousel = () => {
let _timeoutId = null
const startCarouselCycle = () => {
const newTimeoutId = setTimeout(() => {
// Code here that calls scrollToIndex for the FlatList.
}, 5000);
_timeoutId = newTimeoutId;
};
const startNextCarouselCycle = () => {
// Other code here.
startCarouselCycle();
};
useEffect(() => {
startCarouselCycle();
return () => {
// This is called when the screen with the carousel
// is navigated away from, but timeoutId is null.
// Why?!
clearTimeout(_timeoutId);
};
}, []);
return (
<FlatList
// Non-essential code removed.
horizontal={true}
scrollEnabled={false}
onMomentumScrollEnd={startNextCarouselCycle} />
);
};
export default Carousel;
Thanks to everyone for your answers and feedback. I tried implementing what everyone recommended, but to no avail. Luckily, they started me off on the right path, and for all I know, maybe there was something else in my component that I didn't mention in my question that was causing things to be more complex.
All the same, after banging my head against a wall for days, I was able to resolve this problem with the following:
let timeoutId;
const Carousel = () => {
const startCarouselCycle = () => {
timeoutId = setTimeout(() => {
// Code here that calls scrollToIndex for the FlatList.
}, 5000);
};
const startNextCarouselCycle = () => {
// Other code here.
startCarouselCycle();
};
useEffect(() => {
startCarouselCycle();
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);
return (
<FlatList
// Non-essential code removed.
horizontal={true}
scrollEnabled={false}
onMomentumScrollEnd={startNextCarouselCycle}
/>
);
};
export default Carousel;
The main thing I changed was moving the timeoutId variable to outside the component render function. The render function is getting continuously called, which was causing the timeoutId to not properly update (no clue why; some closure issue?!).
All the same, moving the variable outside the Carousel function did the trick.

Categories