React component rendered from object does not get unmounted - javascript

I have the following code, where I need to run clean-up when unmounting each component step. I've set a useEffect on each Step to check if the component has been unmounted. When the parent gets a new currentStep it swaps the currently active component but the clean-up never runs. I'm wondering if this has to do with the nature of the component being rendered from an object
const Step1 = () => {
useEffect(() => {
console.log("doing things here");
return () => {
console.log("clean-up should happen here but this won't print")
}
}, []}
}
const StepMap = {
step1: <Step1/>
step2: <Step2/>
step3: <Step3/>
}
const Parent = ({ currentStep }) => {
return (
<div>
{ StepMap[currentStep] }
</div>
)
}
Alternatively this piece of code does run the clean-up, but I do find the former cleaner
const Parent = ({ currentStep }) => {
return (
<div>
{ currentStep === "step1" && StepMap[currentStep]}
{ currentStep === "step2" && StepMap[currentStep]}
</div>
)
}
Why does the first approach not work? is there a way to make it work like the second while keeping a cleaner implementation?

if you want to write javascript inside jsx we have write it inside {} curly braces like this:
import React, { useEffect, useState } from "react";
const Step1 = () => {
useEffect(() => {
console.log("Step1 doing things here");
return () => {
console.log("Step1 clean-up should happen here but this won't print");
};
}, []);
return <div>stepOne</div>;
};
const Step2 = () => {
useEffect(() => {
console.log("Step2 doing things here");
return () => {
console.log("Step2 clean-up should happen here but this won't print");
};
}, []);
return <div>steptw0</div>;
};
const Step3 = () => {
useEffect(() => {
console.log("Step3 doing things here");
return () => {
console.log("Step3 clean-up should happen here but this won't print");
};
}, []);
return <div>stepthree</div>;
};
export const StepMap = {
step1: <Step1 />,
step2: <Step2 />,
step3: <Step3 />,
};
export const Parent = ({ currentStep }) => {
return <div>{StepMap[currentStep]}</div>;
};
const App = () => {
const [steps, setSteps] = React.useState("step1");
React.useEffect(() => {
setTimeout(() => setSteps("step2"), 5000);
setTimeout(() => setSteps("step3"), 15000);
}, []);
return <Parent currentStep={steps} />;
};
export default App;

Related

TypeError: Cannot read properties of undefined (reading 'lat') using React version-18 #react-google-maps/api with Rapid Api [duplicate]

If we want to restrict useEffect to run only when the component mounts, we can add second parameter of useEffect with [].
useEffect(() => {
// ...
}, []);
But how can we make useEffect to run only when the moment when the component is updated except initial mount?
If you want the useEffect to run only on updates except initial mount, you can make use of useRef to keep track of initialMount with useEffect without the second parameter.
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
// Your useEffect code here to be run on update
}
});
I really like Shubham's response, so I made it a custom Hook
/**
* A custom useEffect hook that only triggers on updates, not on initial mount
* #param {Function} effect
* #param {Array<any>} dependencies
*/
export default function useUpdateEffect(effect, dependencies = []) {
const isInitialMount = useRef(true);
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
return effect();
}
}, dependencies);
}
Both Shubham and Mario suggest the right approach, however the code is still incomplete and does not consider following cases.
If the component unmounts, it should reset it's flag
The passing effect function may have a cleanup function returned from it, that would never get called
Sharing below a more complete code which covers above two missing cases:
import React from 'react';
const useIsMounted = function useIsMounted() {
const isMounted = React.useRef(false);
React.useEffect(function setIsMounted() {
isMounted.current = true;
return function cleanupSetIsMounted() {
isMounted.current = false;
};
}, []);
return isMounted;
};
const useUpdateEffect = function useUpdateEffect(effect, dependencies) {
const isMounted = useIsMounted();
const isInitialMount = React.useRef(true);
React.useEffect(() => {
let effectCleanupFunc = function noop() {};
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
effectCleanupFunc = effect() || effectCleanupFunc;
}
return () => {
effectCleanupFunc();
if (!isMounted.current) {
isInitialMount.current = true;
}
};
}, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Shorter One
const [mounted, setMounted] = useRef(false)
useEffect(() => {
if(!mounted) return setMounted(true)
...
})
React Hook Solution
Hook
export const useMounted = () => {
const mounted = useRef(false)
useEffect(() => {
mounted.current = true
return () => {
mounted.current = false
}
}, [])
return () => mounted.current
}
Usage
const Component = () => {
const mounted = useMounted()
useEffect(() => {
if(!mounted()) return
...
})
}
You can get around it by setting the state to a non-boolean initial value (like a null value) :
const [isCartOpen,setCartOpen] = useState(null);
const [checkout,setCheckout] = useState({});
useEffect(() => {
// check to see if its the initial state
if( isCartOpen === null ){
// first load, set cart to real initial state, after load
setCartOpen( false );
}else if(isCartOpen === false){
// normal on update callback logic
setCartOpen( true );
}
}, [checkout]);
Took help from Subham's answer This code will only run for particular item update not on every update and not on component initial mounting.
const isInitialMount = useRef(true); //useEffect to run only on updates except initial mount
//useEffect to run only on updates except initial mount
useEffect(() => {
if (isInitialMount.current) {
isInitialMount.current = false;
} else {
if(fromScreen!='ht1' && appStatus && timeStamp){
// let timeSpentBG = moment().diff(timeStamp, "seconds");
// let newHeatingTimer = ((bottomTab1Timer > timeSpentBG) ? (bottomTab1Timer - timeSpentBG) : 0);
// dispatch({
// type: types.FT_BOTTOM_TAB_1,
// payload: newHeatingTimer,
// })
// console.log('Appstaatus', appStatus, timeSpentBG, newHeatingTimer)
}
}
}, [appStatus])
To make a custom hook compliant with the rules of hooks you don't need to actually pass dependencies, just wrap your effect function with useCallback
function useEffectOnUpdate(callback) {
const mounted = useRef();
useEffect(() => {
if (!mounted.current) {
mounted.current = true;
} else {
callback();
}
}, [callback]);
};
function SomeComponent({ someProp }) {
useEffectOnUpdate(useCallback(() => {
console.log(someProp);
}, [someProp]));
return <div>sample text</div>;
}
If you tried Shubham's answer, and the useeffect is still being called on the initial mount, you can easily fix this by disabling React strictmode. But if you don't want to disable strictmode, use this.
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
If you tried Shubham's answer, and the useeffect is still being called on the initial mount, and you are using React 18, you can easily fix this by disabling React strictmode. But if you don't want to disable strictmode, use this.
// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = [];
const [state, setState] = useState(init);
const dummyState = useRef(init);
useEffect(() => {
// Compare the old state with the new state
if (dummyState.current == state) {
// This means that the component is mounting
} else {
// This means that the component updated.
dummyState.current = state;
}
}, [state]);
Works in development mode...
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
And in production.
function App() {
const init = [];
const [state, setState] = React.useState(init);
const dummyState = React.useRef(init);
React.useEffect(() => {
if (dummyState.current == state) {
console.log('mount');
} else {
console.log('update');
dummyState.current = state;
}
}, [state]);
return (
<button onClick={() => setState([...state, Math.random()])}>Update state </button>
);
}
ReactDOM.createRoot(document.getElementById("app")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Use the Cleanup function of the useEffect without using an empty array as a second parameter:
useEffect(() => {
return () => {
// your code to be run on update only.
}
});
You can use another useEffect (with an empty array as a second parameter) for initial mount, where you place your code in its main function.

How to call a function after setting state is complete in useEffect?

I would like to run customFunction only when customEffect has finished setting isReady state. And customFunction should only run once no matter if the isReady was set to false or true as long as it was ran after it was set.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
customFunction();
}, []);
return (
<>Hello World</>
)
}
I tried to add isReady as second useEffect argument, but then my customFunction is being run before the customEffect finishes and then again after the isReady is being set.
Also tried having in a separate useEffect, but still seems to run before the customEffect finishes.
Set initial value to null and use separate useEffect as Kevin suggested (only without checking isReady true/false).
In this case setIsReady will change isReady from null to true/false and the second useEffect will be called.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(null);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response);
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (null === isReady) {
return;
}
customFunction();
}, [isReady]);
return (
<>Hello World</>
)
}
Since you want to cue an effect to run after the isReady state is set, and the value of isReady is irrelevant you can to use a second state value to indicate the first effect and state update has completed.
This will trigger the second effect to invoke customFunction but you don't want your component to remain in this state as from here any time the component rerenders the conditions will still be met. You'll want a third "state" to indicate the second effect has been triggered. Here you can use a React ref to indicate this.
export const smallComponent = () => {
const [readySet, setReadySet] = useState(false);
const [isReady, setIsReady] = useState(false);
const customFunctionRunRef = useRef(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
setReadySet(true); // to trigger second effect callback
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (readySet && !customFunctionRunRef.current) {
// won't run before readySet is true
// won't run after customFunctionRunRef true
customFunction();
customFunctionRunRef.current = true;
}
}, [readySet]);
return (
<>Hello World</>
);
}
Better solution borrowed from #p1uton. Use null isReady state to indicate customFunction shouldn't invoke yet, and the ref to keep it from being invoked after.
export const smallComponent = () => {
const [isReady, setIsReady] = useState(null);
const customFunctionRunRef = useRef(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response); // response can be true or false
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (isReady !== null && !customFunctionRunRef.current) {
// won't run before isReady is non-null
// won't run after customFunctionRunRef true
customFunction();
customFunctionRunRef.current = true;
}
}, [isReady]);
return (
<>Hello World</>
);
}
I'm not sure if I understood you correctly, but this is how I would use a separate useEffect.
import customFunction from 'myFile';
export const smallComponent = () => {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const customEffect = async () => {
try {
const response = await get(
`some-api.com`,
);
return setIsReady(response);
} catch {
return null;
}
};
customEffect();
}, []);
useEffect(() => {
if (!isReady) {
return;
}
customFunction();
}, [isReady]);
return (
<>Hello World</>
)
}
Have you tried using this package, isMounted?
I used that in my projects.
import React, { useState, useEffect } from 'react';
import useIsMounted from 'ismounted';
import myService from './myService';
import Loading from './Loading';
import ResultsView from './ResultsView';
const MySecureComponent = () => {
const isMounted = useIsMounted();
const [results, setResults] = useState(null);
useEffect(() => {
myService.getResults().then(val => {
if (isMounted.current) {
setResults(val);
}
});
}, [myService.getResults]);
return results ? <ResultsView results={results} /> : <Loading />;
};
export default MySecureComponent;
https://www.npmjs.com/package/ismounted

"React" how to pass event to adjacent component

I need to make a click on the button in one component and on this click call a function in the adjacent one. What's the easiest way?
I implemented like this. https://stackblitz.com/edit/react-l5beyi But I think you can do it much easier. React is new to me, and this construction looks strange ...
const App = () => {
const [isAdded, setIsAdded] = useState(false);
function handleClick(status) {
setIsAdded(status)
}
return (
<div>
<ComponentFirst
HandleClick={handleClick}
/>
<ComponentSecond
isAdded={isAdded}
handleCreate={handleClick}
/>
</div>
);
}
const ComponentFirst = ({ HandleClick }) => {
return (
<button
onClick={HandleClick}
>button</button>
)
}
const ComponentSecond = (props) => {
let { isAdded, handleCreate } = props;
const result = () => {
alert('work')
console.log('work')
}
React.useEffect(() => {
if (isAdded) {
result()
handleCreate(false);
}
}, [isAdded, handleCreate]);
return (
<></>
)
}
In your (contrived, I suppose) example the second component doesn't render anything, so it doesn't exist. The work should be done by the parent component:
const App = () => {
const handleClick = React.useCallback((status) => {
alert(`work ${status}`);
// or maybe trigger some asynchronous work?
}, []);
return (
<div>
<ComponentFirst handleClick={handleClick} />
</div>
);
};
const ComponentFirst = ({ handleClick }) => {
return <button onClick={() => handleClick("First!")}>button</button>;
};
You can also use a CustomEvent to which any component can listen to.
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
return (
<div>
<ComponentFirst />
<ComponentSecond />
</div>
);
}
const ComponentFirst = () => {
const handleClick = (e) => {
// Create the event.
const event = new CustomEvent("myCustomEventName", {
detail: "Some information"
});
// target can be any Element or other EventTarget.
window.dispatchEvent(event);
};
return <button onClick={handleClick}>button</button>;
};
const ComponentSecond = (props) => {
function eventHandler(e) {
console.log("Dispatched Detail: " + e.detail);
}
//Listen for this event on the window object
useEffect(() => {
window.addEventListener("myCustomEventName", eventHandler);
return () => {
window.removeEventListener("myCustomEventName", eventHandler);
};
});
return <></>;
};

transform Class based component to functional based component

guys i wanna convert this code:
export default class App extends Component {
constructor(props) {
super(props);
this.state = { isLoading: true };
}
performTimeConsumingTask = async () => {
return new Promise((resolve) =>
setTimeout(() => {
resolve('result');
}, 2000)
);
};
async componentDidMount() {
const data = await this.performTimeConsumingTask();
if (data !== null) this.setState({ isLoading: false });
}
render() {
if (this.state.isLoading) return <SplashScreen />;
const { state, navigate } = this.props.navigation;
return (something)
i wrote this code but it doesn`t work :
const App = () => {
const [fontLoaded, setFontLoaded] = useState(false);
const [isTimerOn, setIsTimerOn] = useState(true);
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
/>
);
}
useEffect(async () => {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
});
if (isTimerOn) return <SplashScreen />;
else {
return (something)
This will show an error :
Invariant Violation: Rendered More Hooks than during the previous render.
If I comment the useEffect hook it will run the splashScreen. Can any one help me in converting it?
Pass [] as an argument if you wanted to use this hook as componentDidMount
useEffect(async () => {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
}, []);
Here is a list of hooks how you can use hooks to replace lifecycle methods
https://medium.com/javascript-in-plain-english/lifecycle-methods-substitute-with-react-hooks-b173073052a
The Reason for getting an error is your component is rendering too many times and useEffect is also running on each render by passing [] will run the useEffect on first render as it will behave like componentDidMount.
Also follow this to make network calls inside useEffect
https://medium.com/javascript-in-plain-english/handling-api-calls-using-async-await-in-useeffect-hook-990fb4ae423
There must be no conditional return before using all the hooks, in your case you return before using useEffect.
Also useEffect must not run on every render since it sets state in your case. Since you only want it to run on initial render pass an empty array as the second argument.
Also useEffect callback function cannot be async.
Read more about useEffect hook in the documentation.
Check updated code below
const App = () => {
const [fontLoaded, setFontLoaded] = useState(false);
const [isTimerOn, setIsTimerOn] = useState(true);
const performTimeConsumingTask = async () => {
return new Promise((resolve) =>
setTimeout(() => {
resolve('result');
}, 2000)
);
};
useEffect(() => {
async function myFunction() {
const data = await performTimeConsumingTask();
if (data !== null) setIsTimerOn(false);
}
myFunction();
}, []); // With empty dependency it runs on initial render only like componentDidMount
if (!fontLoaded) {
return (
<AppLoading
startAsync={fetchFonts}
onFinish={() => setFontLoaded(true)}
/>
);
}
if (isTimerOn) return <SplashScreen />;
else {
return (something)

Can't perform a React state update on an unmounted component theme provider

I need help because I get the following 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 the componentWillUnmount method. in createCategory (at themeProvider.js:39)
/* Imports */
import React, { useContext, useState, useEffect } from 'react';
import AsyncStorage from '#react-native-community/async-storage';
import THEMES from '#app/theme/themes.json';
/* /Imports/ */
const STORAGE_KEY = 'THEME_ID';
const ThemeContext = React.createContext();
/* Exports */
export const ThemeContextProvider = ({ children }) => {
const [themeID, setThemeID] = useState();
useEffect(() => {
(async () => {
const storedThemeID = await AsyncStorage.getItem(STORAGE_KEY);
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
})();
}, []);
return (
<ThemeContext.Provider value={{ themeID, setThemeID }}>
{!!themeID ? children : null}
</ThemeContext.Provider>
);
};
export function withTheme(Component) {
function TargetComponent(props) {
const { themeID, setThemeID } = useContext(ThemeContext);
const getTheme = themeID => THEMES.find(theme => theme.key === themeID);
const setTheme = themeID => {
AsyncStorage.setItem(STORAGE_KEY, themeID);
setThemeID(themeID);
};
return (
<Component
{...props}
themes={THEMES}
theme={getTheme(themeID)}
setTheme={setTheme}
/>
);
}
TargetComponent.navigationOptions = Component.navigationOptions;
return TargetComponent;
}
/* /Exports/ */
If you don't already know - you can return a function at the end of your useEffect hook. That function will be called whenever that effect is fired again (e.g. when the values of its dependencies have changed), as well as right before the component unmounts. So if you have a useEffect hook that looks like this:
useEffect(() => {
// logic here
return () => {
// clean up
};
}, []); // no dependencies!
Is equivalent to this:
class SomeComponent extends React.Component {
componentDidMount() {
// logic here
}
componentWillUnmount() {
// clean up
}
}
So in your code I'd add this:
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
try {
// fetch logic omitted...
const data = await AsyncStorage.getItem(STORAGE_KEY);
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
} catch (e) {
throw new Error(e)
}
};
fetchData();
return () => {
isCancelled = true;
};
}, [themeID]);
Try this
let unmounted = false;
useEffect(() => {
(async () => {
const storedThemeID = await AsyncStorage.getItem(STORAGE_KEY);
if (!unmounted) {
if (storedThemeID) setThemeID(storedThemeID);
else setThemeID(THEMES[1].key);
}
})();
return () => {
unmounted = true;
};
}, []);

Categories