React - Race Condition in multiple useEffect's cleanup function - javascript

I have a component that
Initiate a websocket ref on mount (endpoint prop doesn't change)
When component comes into screen view, it calls the subscribe on wsRef
When component goes out of screen view, it calls the unsubscribe on wsRef
The problem is on unmount the 2nd cleanup function fails because we already have closed the websocket by the 1st cleanup function.
So we are unable to unsubscribe as the websocket is not in OPEN state.
How to control the order of cleanup effects here?
function Comp({endpoint, isVisible, id}) {
const wsRef = useRef(null);
useEffect(() => {
wsRef.current = new WebsocketClass(endpoint);
return () => { // [1] cleanup no 1
wsRef.current.close();
wsRef.current = null;
}
}, [endpoint]);
useEffect(() => {
if (isVisible) {
wsRef.current.subscribe(id);
}
return () => {
if (isVisible) {
wsRef.current.unsubscribe(id); // [2] fails, this cleanup runs after [1]
}
}
}, [isVisible, id]);
}
If I re-order the two useEffect in my function, the problem seem to go away, but is it really a permanent solution here?
Also, I can't close the websocket in the unsubscribe cleanup because the component can stay mounted while it goes out of screen and comes back again as user scrolls back and forth.

Related

React inputs stuck and not changing [duplicate]

The useEffect React hook will run the passed-in function on every change. This can be optimized to let it call only when the desired properties change.
What if I want to call an initialization function from componentDidMount and not call it again on changes? Let's say I want to load an entity, but the loading function doesn't need any data from the component. How can we make this using the useEffect hook?
class MyComponent extends React.PureComponent {
componentDidMount() {
loadDataOnlyOnce();
}
render() { ... }
}
With hooks this could look like this:
function MyComponent() {
useEffect(() => {
loadDataOnlyOnce(); // this will fire on every change :(
}, [...???]);
return (...);
}
If you only want to run the function given to useEffect after the initial render, you can give it an empty array as second argument.
function MyComponent() {
useEffect(() => {
loadDataOnlyOnce();
}, []);
return <div> {/* ... */} </div>;
}
TL;DR
useEffect(yourCallback, []) - will trigger the callback only after the first render.
Detailed explanation
useEffect runs by default after every render of the component (thus causing an effect).
When placing useEffect in your component you tell React you want to run the callback as an effect. React will run the effect after rendering and after performing the DOM updates.
If you pass only a callback - the callback will run after each render.
If passing a second argument (array), React will run the callback after the first render and every time one of the elements in the array is changed. for example when placing useEffect(() => console.log('hello'), [someVar, someOtherVar]) - the callback will run after the first render and after any render that one of someVar or someOtherVar are changed.
By passing the second argument an empty array, React will compare after each render the array and will see nothing was changed, thus calling the callback only after the first render.
useMountEffect hook
Running a function only once after component mounts is such a common pattern that it justifies a hook of its own that hides implementation details.
const useMountEffect = (fun) => useEffect(fun, [])
Use it in any functional component.
function MyComponent() {
useMountEffect(function) // function will run only once after it has mounted.
return <div>...</div>;
}
About the useMountEffect hook
When using useEffect with a second array argument, React will run the callback after mounting (initial render) and after values in the array have changed. Since we pass an empty array, it will run only after mounting.
We have to stop thinking in component-life-cycle-methods (i.e. componentDidMount). We have to start thinking in effects. React effects are different from old-style class-life-cycle-methods.
By default effects run after every render cycle, but there are options to opt out from this behaviour. To opt out, you can define dependencies that mean that an effect is only carried out when a change to one of the dependencies is made.
If you explicitly define, that an effect has no dependecy, the effect runs only once, after the first render-cycle.
1st solution (with ESLint-complaint)
So, the first solution for your example would be the following:
function MyComponent() {
const loadDataOnlyOnce = () => {
console.log("loadDataOnlyOnce");
};
useEffect(() => {
loadDataOnlyOnce(); // this will fire only on first render
}, []);
return (...);
}
But then the React Hooks ESLint plugin will complain with something like that:
React Hook useEffect has missing dependency: loadDataOnlyOnce. Either include it or remove the dependency array.
At first this warning seems annoying, but please don't ignore it. It helps you code better and saves you from "stale closures". If you don't know what "stale closures" are, please read this great post.
2nd solution (the right way, if dependency is not dependent on component)
If we add loadDataOnlyOnce to the dependency array, our effect will run after every render-cycle, because the reference of loadDataOnlyOnce changes on every render, because the function is destroyed(garbarge-collected) and a new function is created, but that's exactly what we don't want.
We have to keep the same reference of loadDataOnlyOnce during render-cycles.
So just move the function-definition above:
const loadDataOnlyOnce = () => {
console.log("loadDataOnlyOnce");
};
function MyComponent() {
useEffect(() => {
loadDataOnlyOnce(); // this will fire only on first render
}, [loadDataOnlyOnce]);
return (...);
}
With this change you ensure that the reference of loadDataOnlyOnce will never change. Therefore you can also safely add the reference to the dependency array.
3rd solution (the right way, if dependency is dependent on component)
If the dependency of the effect (loadDataOnlyOnce), is dependent on the component (need props or state), there's React's builtin useCallback-Hook.
An elementary sense of the useCallback-Hook is to keep the reference of a function identical during render-cycles.
function MyComponent() {
const [state, setState] = useState("state");
const loadDataOnlyOnce = useCallback(() => {
console.log(`I need ${state}!!`);
}, [state]);
useEffect(() => {
loadDataOnlyOnce(); // this will fire only when loadDataOnlyOnce-reference changes
}, [loadDataOnlyOnce]);
return (...);
}
function useOnceCall(cb, condition = true) {
const isCalledRef = React.useRef(false);
React.useEffect(() => {
if (condition && !isCalledRef.current) {
isCalledRef.current = true;
cb();
}
}, [cb, condition]);
}
and use it.
useOnceCall(() => {
console.log('called');
})
or
useOnceCall(()=>{
console.log('Fetched Data');
}, isFetched);
Pass an empty array as the second argument to useEffect. This effectively tells React, quoting the docs:
This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
Here's a snippet which you can run to show that it works:
function App() {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
fetch('https://randomuser.me/api/')
.then(results => results.json())
.then(data => {
setUser(data.results[0]);
});
}, []); // Pass empty array to only run once on mount.
return <div>
{user ? user.name.first : 'Loading...'}
</div>;
}
ReactDOM.render(<App/>, document.getElementById('app'));
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
I like to define a mount function, it tricks EsLint in the same way useMount does and I find it more self-explanatory.
const mount = () => {
console.log('mounted')
// ...
const unmount = () => {
console.log('unmounted')
// ...
}
return unmount
}
useEffect(mount, [])
leave the dependency array blank . hope this will help you understand better.
useEffect(() => {
doSomething()
}, [])
empty dependency array runs Only Once, on Mount
useEffect(() => {
doSomething(value)
}, [value])
pass value as a dependency. if dependencies has changed since the last time, the effect will run again.
useEffect(() => {
doSomething(value)
})
no dependency. This gets called after every render.
I had this issue with React 18. This is how I handled it:
import { useEffect, useRef } from "react";
export default function Component() {
const isRunned = useRef(false);
useEffect(() => {
if(isRunned.current) return;
isRunned.current = true;
/* CODE THAT SHOULD RUN ONCE */
}, []);
return <div> content </div>;
}
Check here how they explain why useEffect is called more than once.
Here is my version of Yasin's answer.
import {useEffect, useRef} from 'react';
const useOnceEffect = (effect: () => void) => {
const initialRef = useRef(true);
useEffect(() => {
if (!initialRef.current) {
return;
}
initialRef.current = false;
effect();
}, [effect]);
};
export default useOnceEffect;
Usage:
useOnceEffect(
useCallback(() => {
nonHookFunc(deps1, deps2);
}, [deps1, deps2])
);
This does not answer your question exactly but may have the same intended affect of only running a function once and after the first render. Very similar to the componentDidMount function. This uses useState instead of useEffect to avoid dependency lint errors. You simply pass a self-executing anonymous function as the first argument to useState. As an aside, I'm not sure why React doesn't simply provide a hook that does this.
import React, { useState } from "react"
const Component = () => {
useState((() => {
console.log('componentDidMountHook...')
}))
return (
<div className='component'>Component</div>
)
}
export default Component
I found out after some time spend on the internet. useEffect fires once on component mount, then componennt unmounts and mounts again, useEffect fires again. You have to check more on React docs, why they do that.
So, I used custom hook for that. On unmount you have to change your useRef state. In this case do not forget a return statement: when component unmounts, useEffect runs cleanup function after return.
import React, { useEffect, useRef } from "react"
const useDidMountEffect = (
func: () => void,
deps: React.DependencyList | undefined
) => {
const didMount = useRef(false)
useEffect(() => {
if (didMount.current) {
func()
}
return () => {
didMount.current = true
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps)
}
export default useDidMountEffect
Use it like normal useEffect:
useDidMountEffect(() => {
// your action
}, [])
window.onpageshow works even if the user presses the back button to navigate to the page, unlike passing an empty array as second argument of the use-effect hook which does not fire when returning to the page via the back button (thus not on every form of initial page load).
useEffect(() => {
window.onpageshow = async function() {
setSomeState(false)
let results = await AsyncFunction()
console.log(results, 'Fires on on first load,
refresh, or coming to the page via the back button')
};
};
I found that with the once function from lodash the problem may be solved concisely and elegantly.
import { once } from "lodash";
import { useEffect, useRef } from "react";
export const useEffectOnce = (cb: () => void) => {
const callBackOnce = useRef(once(cb)).current;
useEffect(() => callBackOnce(), [callBackOnce]);
};
Incase you just call the function in useeffect after render you add an empty array as the second argument for the useeffect
useEffect=(()=>{
functionName(firstName,lastName);
},[firstName,lastName])

Why only first time React function component getting props

React throw an error when we try to update the state on an unmounted component.So When I test react component for that I am getting errors on the first render only.
I made a component that enable child component based on click. And child component have button which updates state after some settimeout which throw react warning
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.
Which is perfectly valid. But to overcome this I am passing enable props from the parent component based on that there is a condition just before setTimeout in the child component. So why does it throw an error the first time only?
To reproduce
Click on the child component button which is false and click on parent component button enable which unmount the child component.
**My question is why react throw an error on the first time only ? And why it is working fine on second time **
Parent component
import { useState } from "react";
import "./styles.css";
import { Test } from "./Test";
export default function App() {
const [state, setstate] = useState(true);
const changeState = () => {
setstate(!state);
};
return (
<div className="App">
<button onClick={changeState}>enable </button>
{state && <Test enable={state} />}
</div>
);
}
Child Component
import React, { useState } from "react";
export const Test = (props) => {
const [state, setstate] = useState(false);
const fetchData = () => {
setstate(!state);
if (props.enable) {
setTimeout(() => {
setstate(false);
}, 1000);
}
};
return (
<>
<button onClick={fetchData}> {`${state}`}</button>
</>
);
};
Codesandbox link to test
Nice track, Just you are missing a minor point, when you write a state thats needed some time to execute and the same time we can visit the flow again and again base on any action, then we need to clear old subscription before go to new one...
For example, in your code here, you update state flow, but the state flow is register a new subscription every time we visit a component with valid props and click on button, so that, prev execution may still work when you trigger new event, so simply, what we need to do unmounted old subscription and we can do that by this for your case:
import React, { useState, useEffect } from "react";
export const Test = (props) => {
const [state, setstate] = useState(false);
useEffect(() => {
if (props.enable) {
const timer = setTimeout(() => {
setstate(false);
}, 1000);
return () => clearTimeout(timer);
}
}, [state, props.enable]);
const fetchData = () => {
setstate((prev) => !prev);
};
return (
<>
<button onClick={fetchData}> {`${state}`}</button>
</>
);
};
Look at code above, simply we add code need to cleanup in effect which its look to my state and prop, now when I click on button, the effect will trigger, if we do that again, the clearTime will work for prev subscription and then add new one and so on...
Notes:
In your case we can remove function and use setState direct on your button.
Prefer to use useCallBack in your function like const fetchData = useCAllback...
You can use setstate((prev) => !prev); its will be work as snapshot, and its usefull when you depends on old value..but may it not needed in some cases too, but just to know about this feature.
Update 1:
What is Subscription:
You can say the subscription represents a disposable resource, such as the execution of an Observable. A Subscription has one important method, unsubscribe, that takes no argument and just disposes the resource held by the subscription, in another word, you can say yes, any async task or any job will be invoke to react life-cycle state and its needed to observe changes, then you talk about subscribe, like API or time out or time interval and so on, any of these action thats need to clear prev subscribe (stop observer - unsubscribe) to prevent any memory leek and clear memory to keep state flow safe and prevent unneeded reredner.

React navigation didfocus event listener works differently between class component and functional component

When I transition to this screen, it will do some API calls to fetch the latest data. But it seems not trigger the didFocus event to fire the api calls when I transition from another navigation stack with hooks version while it works well with class version.
How do I make hooks version have the same behavior as class version?
What's the difference between this two version?
class component version
class someScreen extends Component {
componentDidMount() {
const {
navigation,
} = this.props;
this.navFocusListener = navigation.addListener('didFocus', () => {
// do some API calls here
console.log("class version");
API_CALL();
});
}
componentWillUnmount() {
this.navFocusListener.remove();
}
}
console output
transition from other navigation stack to this screen: class version
transition between screens in same stack: class version
Hooks version
const someScreen = ({
navigation,
}) => {
useEffect(() => {
const navFocusListener = navigation.addListener('didFocus', () => {
// do some API calls here
API_CALL();
console.log('hooooks');
});
return () => {
navFocusListener.remove();
};
}, []);
}
console output
transition from other navigation stack to this screen: nothing is shown in console
transition between screens in same stack: hooooks
BTW, here is the workaround solution I found
const someScreen = ({
navigation,
}) => {
useEffect(() => {
const isFocused = navigation.isFocused();
// manually judge if the screen is focused
// if did, fire api call
if (isFocused) {
// do the same API calls here
API_CALL();
console.log('focused section');
}
const navFocusListener = navigation.addListener('didFocus', () => {
// do some API calls here
API_CALL();
console.log('listener section');
});
return () => {
navFocusListener.remove();
};
}, []);
}
console output
transition from other navigation stack to this screen: focused section
transition between screens in same stack: listener section
I guess I found the root cause of the inconsistent behavior.
There is another hook called useLayoutEffect
useLayoutEffect
The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
the useLayoutEffect will block the painting while the useEffect will not.
That confirm and explains my guess that the didFocus event had fired, but it didn't trigger the listener since it miss the timing
so in my case, I have to use useLayoutEffect instead of useEffect
reference: https://kentcdodds.com/blog/useeffect-vs-uselayouteffect
https://reactjs.org/docs/hooks-reference.html#uselayouteffect

Why is the cleanup function from `useEffect` called on every render?

I've been learning React and I read that the function returned from useEffect is meant to do cleanup and React performs the cleanup when the component unmounts.
So I experimented with it a bit but found in the following example that the function was called every time the component re-renders as opposed to only the time it got unmounted from the DOM, i.e. it console.log("unmount"); every time the component re-renders.
Why is that?
function Something({ setShow }) {
const [array, setArray] = useState([]);
const myRef = useRef(null);
useEffect(() => {
const id = setInterval(() => {
setArray(array.concat("hello"));
}, 3000);
myRef.current = id;
return () => {
console.log("unmount");
clearInterval(myRef.current);
};
}, [array]);
const unmount = () => {
setShow(false);
};
return (
<div>
{array.map((item, index) => {
return (
<p key={index}>
{Array(index + 1)
.fill(item)
.join("")}
</p>
);
})}
<button onClick={() => unmount()}>close</button>
</div>
);
}
function App() {
const [show, setShow] = useState(true);
return show ? <Something setShow={setShow} /> : null;
}
Live example: https://codesandbox.io/s/vigilant-leavitt-z1jd2
React performs the cleanup when the component unmounts.
I'm not sure where you read this but this statement is incorrect. React performs the cleanup when the dependencies to that hook changes and the effect hook needs to run again with new values. This behaviour is intentional to maintain the reactivity of the view to changing data. Going off the official example, let's say an app subscribes to status updates from a friends' profile. Being the great friend you are, you are decide to unfriend them and befriend someone else. Now the app needs to unsubscribe from the previous friend's status updates and listen to updates from your new friend. This is natural and easy to achieve with the way useEffect works.
useEffect(() => {
chatAPI.subscribe(props.friend.id);
return () => chatAPI.unsubscribe(props.friend.id);
}, [ props.friend.id ])
By including the friend id in the dependency list, we can indicate that the hook needs to run only when the friend id changes.
In your example you have specified the array in the dependency list and you are changing the array at a set interval. Every time you change the array, the hook reruns.
You can achieve the correct functionality simply by removing the array from the dependency list and using the callback version of the setState hook. The callback version always operates on the previous version of the state, so there is no need to refresh the hook every time the array changes.
useEffect(() => {
const id = setInterval(() => setArray(array => [ ...array, "hello" ]), 3000);
return () => {
console.log("unmount");
clearInterval(id);
};
}, []);
Some additional feedback would be to use the id directly in clearInterval as the value is closed upon (captured) when you create the cleanup function. There is no need to save it to a ref.
The React docs have an explanation section exactly on this.
In short, the reason is because such design protects against stale data and update bugs.
The useEffect hook in React is designed to handle both the initial render and any subsequent renders (here's more about it).
Effects are controlled via their dependencies, not by the lifecycle of the component that uses them.
Anytime dependencies of an effect change, useEffect will cleanup the previous effect and run the new effect.
Such design is more predictable - each render has its own independent (pure) behavioral effect. This makes sure that the UI always shows the correct data (since the UI in React's mental model is a screenshot of the state for a particular render).
The way we control effects is through their dependencies.
To prevent cleanup from running on every render, we just have to not change the dependencies of the effect.
In your case concretely, the cleanup is happening because array is changing, i.e. Object.is(oldArray, newArray) === false
useEffect(() => {
// ...
}, [array]);
// ^^^^^ you're changing the dependency of the effect
You're causing this change with the following line:
useEffect(() => {
const id = setInterval(() => {
setArray(array.concat("hello")); // <-- changing the array changes the effect dep
}, 3000);
myRef.current = id;
return () => {
clearInterval(myRef.current);
};
}, [array]); // <-- the array is the effect dep
As others have said, the useEffect was depending on the changes of "array" that was specified in the 2nd parameter in the useEffect. So by setting it to empty array, that'd help to trigger useEffect once when the component mounted.
The trick here is to change the previous state of the Array.
setArray((arr) => arr.concat("hello"));
See below:
useEffect(() => {
const id = setInterval(() => {
setArray((arr) => arr.concat("hello"));
}, 3000);
myRef.current = id;
return () => {
console.log("unmount");
clearInterval(myRef.current);
};
}, []);
I forked your CodeSandbox for demonstration:
https://codesandbox.io/s/heuristic-maxwell-gcuf7?file=/src/index.js
Looking at the code I could guess its because of the second param [array]. You are updating it, so it will call a re-render. Try setting an empty array.
Every state update will call a re-render and unmount, and that array is changing.
It seems expected. As per the documentation here, useEffect is called after first render, every update and unmount.
https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Tip
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate and before
componentWillUnmount combined.
This is a Jest test that shows the render and effect order.
As you can see from the expect, once the dependency foo changes due to the state update it triggers a NEW render followed by the cleanup function of the first render.
it("with useEffect async set state and timeout and cleanup", async () => {
jest.useFakeTimers();
let theRenderCount = 0;
const trackFn = jest.fn((label: string) => { });
function MyComponent() {
const renderCount = theRenderCount;
const [foo, setFoo] = useState("foo");
useEffect(() => {
trackFn(`useEffect ${renderCount}`);
(async () => {
await new Promise<string>((resolve) =>
setTimeout(() => resolve("bar"), 5000)
);
setFoo("bar");
})();
return () => trackFn(`useEffect cleanup ${renderCount}`);
}, [foo]);
++theRenderCount;
trackFn(`render ${renderCount}`);
return <span data-testid="asdf">{foo}</span>;
}
const { unmount } = render(<MyComponent></MyComponent>);
expect(screen.getByTestId("asdf").textContent).toBe("foo");
jest.advanceTimersByTime(4999);
expect(screen.getByTestId("asdf").textContent).toBe("foo");
jest.advanceTimersByTime(1);
await waitFor(() =>
expect(screen.getByTestId("asdf").textContent).toBe("bar")
);
trackFn("before unmount");
unmount();
expect(trackFn.mock.calls).toEqual([
['render 0'],
['useEffect 0'],
['render 1'],
['useEffect cleanup 0'],
['useEffect 1'],
['before unmount'],
['useEffect cleanup 1']
])
});

How to set up firebase auth onAuthStateChanged() listener run when React app is loaded?

I'm trying set up the Firebase auth listener on my top level component App in order to provide the authUser object via React context to all other components.
I'm currently doing this:
function App() {
console.log('Rendering App...');
const [authUser,setAuthUser] = useState(null);
const [authWasListened,setAuthWasListened] = useState(false);
useEffect(()=>{
console.log('Running App useEffect...');
return firebase.auth().onAuthStateChanged(
(authUser) => {
console.log(authUser);
console.log(authUser.uid);
if(authUser) {
setAuthUser(authUser);
setAuthWasListened(true);
} else {
setAuthUser(null);
setAuthWasListened(true);
}
}
);
},[]);
return(
authWasListened ?
<Layout/>
: <div>Waiting for auth...</div>
);
}
But I'm getting the log output:
Rendering App...
Running App useEffect...
It seems that I'm setting up the listener but it doesn't run at first, therefore it's not getting the current auth state (which is null, since I don't even have a login form yet). It seems like it's waiting for a change to occur.
Shouldn't the authListener get the current authUser state at first, and then listen to changes? What am I doing wrong?
UPDATE
I've found out what I was doing wrong. The useEffect should return a function to clear the listener on unmount and not a function call, as I was trying to do above. So my listener was never being set up.
This is working as intended now:
useEffect(()=>{
console.log('Running App useEffect...');
const authListener = firebase.auth().onAuthStateChanged(
(authUser) => {
console.log(authUser);
console.log(authUser.uid);
if(authUser) {
setAuthUser(authUser);
setAuthWasListened(true);
} else {
setAuthUser(null);
setAuthWasListened(true);
}
}
);
return authListener; // THIS MUST BE A FUNCTION, AND NOT A FUNCTION CALL
},[]);
See https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
Why did we return a function from our effect? This is the optional cleanup mechanism for effects. Every effect may return a function that cleans up after it. This lets us keep the logic for adding and removing subscriptions close to each other. They’re part of the same effect!
When exactly does React clean up an effect? React performs the cleanup when the component unmounts. However, as we learned earlier, effects run for every render and not just once. This is why React also cleans up effects from the previous render before running the effects next time. We’ll discuss why this helps avoid bugs and how to opt out of this behavior in case it creates performance issues later below.
And see Set an authentication state observer and get user data
Try the following code.
function App() {
console.log('Rendering App...');
const [authUser,setAuthUser] = useState(null);
const [authWasListened,setAuthWasListened] = useState(false);
useEffect(()=>{
console.log('Running App useEffect...');
firebase.auth().onAuthStateChanged(
(authUser) => {
console.log(authUser);
console.log(authUser.uid);
if(authUser) {
setAuthUser(authUser);
setAuthWasListened(true);
} else {
setAuthUser(null);
setAuthWasListened(true);
}
}
);
},[]);
return(
authWasListened ?
<Layout/>
: <div>Waiting for auth...</div>
);
}
The listener is not immediate. The change in state might take some time from the moment the page is first loaded. If you want an immediate check to see if a user is signed in, you can use currentUser, but again, it might take some time for that to update, so the listener is the better choice.

Categories