How to Unmount React Functional Component? - reactjs

I've built several modals as React functional components. They were shown/hidden via an isModalOpen boolean property in the modal's associated Context. This has worked great.
Now, for various reasons, a colleague needs me to refactor this code and instead control the visibility of the modal at one level higher. Here's some sample code:
import React, { useState } from 'react';
import Button from 'react-bootstrap/Button';
import { UsersProvider } from '../../../contexts/UsersContext';
import AddUsers from './AddUsers';
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
export default AddUsersLauncher;
This all works great initially. A button is rendered and when that button is pressed then the modal is shown.
The problem lies with how to hide it. Before I was just setting isModalOpen to false in the reducer.
When I had a quick conversation with my colleague earlier today, he said that the code above would work and I wouldn't have to pass anything into AddUsers. I'm thinking though that I need to pass the setShowModal function into the component as it could then be called to hide the modal.
But I'm open to the possibility that I'm not seeing a much simpler way to do this. Might there be?

To call something on unmount you can use useEffect. Whatever you return in the useEffect, that will be called on unmount. For example, in your case
const AddUsersLauncher = () => {
const [showModal, setShowModal] = useState(false);
useEffect(() => {
return () => {
// Your code you want to run on unmount.
};
}, []);
return (
<div>
<UsersProvider>
<Button onClick={() => setShowModal(true)}>Add Users</Button>
{showModal && <AddUsers />}
</UsersProvider>
</div>
);
};
Second argument of the useEffect accepts an array, which diff the value of elements to check whether to call useEffect again. Here, I passed empty array [], so, it will call useEffect only once.
If you have passed something else, lets say, showModal in the array, then whenever showModal value will change, useEffect will call, and will call the returned function if specified.

If you want to leave showModal as state variable in AddUsersLauncher and change it from within AddUsers, then yes, you have to pass the reference of setShowModal to AddUsers. State management in React can become messy in two-way data flows, so I would advise you to have a look at Redux for storing and changing state shared by multiple components

Related

React component does re-render, however effect does not run whereas it should

This is my Component:
function Test() {
const [data, setData]=useState<Array<string>>([]);
console.log('Hello:', data);
useEffect(()=>{
console.log('data: ', data)
}, [data])
const test1 = () => {
setData(data.concat('a'));
}
const test2 = () => {
setData(data);
}
return (
<>
<button onClick={test1}>Button one</button>
<button onClick={test2}>Button two</button>
</>
);
}
Everything works fine when clicking Button one. Component does re-render and effect does run. However, what happens with Button two is something I cannot explain:
If clicking Button two right after Button one, the Component does re-render but effect does not run. That makes no sense since React is using Object.is comparison for deciding if it should re-render/run the effect. How does this comparison produce different results among useState and useEffect? At first it decides to re-render, that means state value data has changed. How is that true with setData(data)? Then, it decides not to run the effect, that means there is no change in data dependency. Obviously, there is a contradiction among the above two decisions...
If clicking Button two for a second time (after having clicked on Button one), then nothing happens which does make sense absolutely.
Could someone explain the above behaviour?
If the new state is the same as the current state, as you mentioned by Object.is React will skip the re-render but as mentioned in the React useState docs React might still call the component.
Although in some cases React may still need to call your component before skipping the children, it shouldn’t affect your code.
This results in running the console.log with the "Hello: ", data values.
And so, React does not actually re-render the component.
We can see this with a useEffect with no dependency array which acccording to the React useEffect docs should run every re-render.
Effect will re-run after every re-render of the component.
useEffect(() => {
console.warn("Render");
});
As you can see this is not the case.
const {useState, useEffect, Fragment} = React;
function Test() {
const [data, setData] = useState([]);
console.log("Hello:", data);
useEffect(() => {
console.warn("Render");
});
useEffect(() => {
console.log("data: ", data);
}, [data]);
const test1 = () => {
setData(data.concat("a"));
};
const test2 = () => {
setData(data);
};
return (
<Fragment>
<button onClick={test1}>Button one</button>
<button onClick={test2}>Button two</button>
</Fragment>
);
}
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Test />
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
what happened is when you use setData(data); the Component Re-renders no matter if the Data has actually changed or not, but when it comes to useEffect the way it compares values it takes the old values and compares it with the new ones if the values have changed it will execute what inside it otherwise it won't, this behavior will also happen if the data is an object it will check each value inside it to decide whether the object has changed or not

Prevent useState value from being reset when props change

I have a component that looks something like this:
//#flow
import React, { useState } from "react";
type Props = {
likes: int,
toggleLike: () => void,
};
const Foo = (props: Props) => {
const [open, setOpen] = useState(false);
const style = `item${open ? " open": ""}`;
return (
<div className={style} onMouseOver={() => setOpen(true)} onFocus={() => setOpen(true)} onMouseOut={() => setOpen(false)} onBlur={() => setOpen(false)}>
<button onClick={props.toggleLike}>Toggle like</button>
</div>
);
};
export default Foo;
The open state is used to apply the "open" class when moused over. The problem comes if I call the toggleLike() prop function, since this updates the props and the component is rerendered with open reset to false. As the style uses a transition, this results in the animation rerunning as it changes back to false, then to true due to the mouse being over it.
So, how can I prevent open being reset back to false on each subsequent render? It seems like it should be straightforward, but after going through https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state I can't seem to apply it in my case.
State does not reset when props change. State is on a per component basis and is preserved throughout re-renders, hence being called "state".
As Dennis Vash already mentioned, the problem is most likely caused by the component being unmounted or replaced by an identical component. You can verify this easily by adding this to your component:
useEffect(() => {
console.log("Mounted")
}, [])
You should see multiple "Mounted" in the console.
If there's no way to prevent the component from being replaced or unmounted, consider putting the state into a context and consume that context inside your component, as you can also wrap each of your components into its own context to give it a unique, non-global, state.

useEffect hook - dependencies - re-render issues

The first case:
_Let's say I have a prop that is in redux state or in parent state.
_I do not want the useEffect to fire whenever this prop changes,
_But I do need to use the prop within the useEffect.
_React warns me to add the prop to the dependency array, but if I do so, then the useEffect will fire again.
The second case:
_I am using a function within a useEffect,
_But the function is also needed elsewhere.
_Don't want to duplicate the function code.
_React wants me to add the function to the dependency array, but I don't want the useEffect to fire every time that function reference changes.
While working with useEffect you should think about closures. If any props or local variable is used inside the useEffect then it is advised to include it inside your dependency array else you will be working with the stale data due to the closure.
Here I present a use case. Inside ComponentUsingRef we are using ref which works as the container. You can find more about it at https://reactjs.org/docs/hooks-reference.html#useref
Advantage of this approach is that you wont be bound to memoize fn in
your parent component. You will always be using latest value of
function and in the given case it won't even cause firing of useEffect as it won't be on your dependency
const Component=({fn})=>{
useEffect(()=>{
fn()
},[fn])
.....
.....
return <SomeComponent {...newProps}/>
}
const ComponentUsingRef=({fn}){
const fnRef = useRef(fn)
fnRef.current = fn // so that our ref container contains the latest value
useEffect(()=>{
fn.current()
},[ ])
.....
.....
return <SomeComponent {...newProps}/>
}
If you want to use abstraction then you should extract your logic inside the custom hook
If the only issue is the warning then don't worry about it, you can simply disable the rule for your effect and omit the prop from the dependency array if it's the only prop being used in the effect. For effects with multiple dependencies, you can use useRef to store the latest value of the prop without firing the effect, like this:
const Comp = ({someProp, ...props}) => {
const myProp = useRef(someProp);
myProp.current = someProp;
// this effect won't fire when someProp is changed
React.useEffect(() => {
doSomething(myProp.current, props.a, props.b);
}, [props.a, props.b, myProp]);
}
For your second case, I'd probably put that function in a separate file and import it in the components where it'll be used, however, you can use the same pattern for your second case.
To ignore the warnings react gives you, you can disable them by adding // eslint-disable-next-line react-hooks/exhaustive-deps to the useEffect code. You can read more about this in the Rules of Hooks section in the React documentation. These rules are included in the eslint-plugin-react-hooks package.
To prevent the useEffect from firing everytime the function reference changes, you can use useCallback. The useCallback hook will store the reference to the function instead of the function itself. The reference of the function will only be updated when one of the dependencies of the function is updated. If you don't want the function reference to be updated ever, you can leave the dependency array empty in the same way as the dependency array of the useEffect hook.
Here is a working example of both:
import React, { useState, useEffect, useCallback } from "react";
import "./styles.css";
export default function App() {
const [value, setValue] = useState(0);
const wrappedFunction = useCallback(
(caller) => {
const newValue = value + 1;
setValue(newValue);
console.log(`Firing function from ${caller}: ${newValue}`);
},
[value]
);
useEffect(() => {
console.log(`Logging from useEffect: ${value}`);
wrappedFunction("useEffect");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>{value}</div>
<button onClick={() => wrappedFunction("button click")}>
Fire function
</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
Refer this link for the First Problem Sandbox
Here we are just using a state variable to hold the initial value, whether its coming from the Parent or Store, so Even if the value change inside the Parent or store, the child will still be on the older value, until unless it updates it using useEffect.
Parent
export default function App() {
const [state, setState] = useState("monika");
return (
<div className="App">
<h1>My Name is {state}</h1>
<button
onClick={() => {
setState("Rohan");
}}
>
change name
</button>
<First username={state} />
</div>
);
}
Child
export default function First({ username }) {
const [name, setName] = useState(username);
return (
<div>
<h1>My Name is {name}</h1>
</div>
);
}

Hooks parent unmounted before children

I' working on react since few months. I started Hooks since few days (I know quite late) the thing is, compare to react component the life cycle methodes it's look like they are different on some points.
The useEffect hook can reproduce :
-componentDidMount();
-componentDidUpdate();
-componentWillUnMount();
But I observe a difference between react's component and function it's about the way how function is unmounted. I noted the unmount methode, compare to the react's component,the react's function unmount the parent before the child/ren
import React, { ReactElement, useEffect, useState } from "react";
import { useLocation, useHistory } from "react-router-dom";
export function Child2({
count,
childrenUnmounted,
}: {
count: number;
childrenUnmounted: Function;
}): ReactElement {
useEffect(() => {
return () => {
console.log("Unmounted");
childrenUnmounted(count);
};
}, [, count]);
return (
<div>
<h2>Unmouted</h2>
</div>
);
}
export function Child1({ count }: { count: number }): ReactElement {
const [validation, setValidation] = useState(false);
const usehistory = useHistory();
const childrenUnmounted = (count: number) => {
console.log("validation", validation, count);
setValidation(false);
};
const changeUrl = () => {
setValidation(true);
usehistory.push("http://localhost:3000/${count}");
};
return (
<div>
<h2>incremente</h2>
<Child2
count={count}
childrenUnmounted={(count: number) => childrenUnmounted(count)}
/>
<button className="button" onClick={() => changeUrl()}>
validation
</button>
<button
className="button"
onClick={() => usehistory.push(`http://localhost:3000/${count}`)}
>
nope
</button>
</div>
);
}
export default function Parent(): ReactElement {
const [count, setcount] = useState(-1);
const location = useLocation();
useEffect(() => {
setcount(count + 1);
}, [, location]);
return (
<div>
<h2>hello</h2>
<h3>{count}</h3>
<Child1 count={count} />
</div>
);
}
With the code above something annoying happen, when you clicked on the validation button. Value in the Child1is at true, at the moment of the click, and it's change the URL to trigger a rerender of the Parent to change the data (here count).
The thing I don't understand is why at the unmount of the Child2, at the childrenUnmounted(count) called (to trigger the same function but in the Child1) in the Child1 the validation is equal to false even the validation was clicked ? and when you click on nope just after validation you got true... it's look like the Child1 do not matter of the current state of the validation (he use the previous state)
Someone could help me to understand what's going on ?
Thx of the help.
SOLUTION:
I used useRef instead of useState from the validation to don't depend of the re-render as Giovanni Esposito said :
because hooks are async and you could not get the last value setted for state
So useRef was my solution
Ciao, I think you problem is related on when you logs validation value. I explain better.
Your parent relationship are: Parent -> Child1 -> Child2. Ok.
Now you click validation button on Child2. validation button calls changeUrl that calls usehistory.push("http://localhost:3000/${count}"); and starts to change validation value (why starts? because setValidation is async).
If the unmounting of Child2 comes now, could be that validation value is no yet setted by async setValidation (and log returns the old value for validation).
Well, at some point this setValidation finished and sets validation to true. Now you click nope button and you get true for validation (the last value setted).
So, to make the story short, I think that what you are seeing in logs it's just because hooks are async and you could not get the last value setted for state (if you use log in this way). The only way you have to log always the last value setted is useEffect hook with value you want to log in deps list.

What invokves the 2nd function call when using React Hooks?

I wrote the following React exercise which uses no hooks and renders a button.
const Button = ({ onClick }) => <button onClick={onClick}>Do Nothing</button>;
const Base = () => {
const onClickFunction = (() => {
console.log("Creating OnClick Function");
return () => {};
})();
return (
<div className="App">
<h1>Hello</h1>
<Button onClick={onClickFunction} />
</div>
);
};
onClickFunction uses a self-invoking function, so that I can place a console.log to see the following behaviour. In this example, when Base is rendered, the message Creating OnClick Function appears only once 👍
If I change Base to the following however, adding a hook usage:
const Button = ({ onClick }) => <button onClick={onClick}>Do Nothing</button>;
const Base = () => {
const notUsedRef = React.useRef();
const onClickFunction = (() => {
console.log("Creating OnClick Function");
return () => {};
})();
return (
<div className="App">
<h1>Hello</h1>
<Button onClick={onClickFunction} />
</div>
);
};
You will see the Creating OnClick Function message twice.
This CodeSandbox illustrates what I've been seeing: https://codesandbox.io/s/dawn-forest-99clo?file=/src/App.js
Using React DevTools Profiler, we can see there is no rerender of this component.
Using <React.Profiler, it reports this component also didn't update.
I know that using React.useCallback wouldn't trigger a second invokation, however the question would still stand why we are in the situation Base is called twice.
My question is: why and what is triggering Base to be invoked when there is no need for a rerender.
This is due to the way React implements hooks.
If you invoke any hook, even if you don't use the resulting value, you are telling React to render twice before mounting, even if the props don't change. You can substitute the usage of useRef by useState, useEffect, etc. Try below.
You can also wrap your component with React.memo. Every function defined inside the function is recreated in every render.
https://codesandbox.io/s/elastic-water-y18w0?file=/src/App.js
EDIT: Only happens during development and in components wrapped by React.StrictMode. In the words of gaearon:
It's an intentional feature of the StrictMode. This only happens in
development, and helps find accidental side effects put into the
render phase. We only do this for components with Hooks because those
are more likely to accidentally have side effects in the wrong place.
https://github.com/facebook/react/issues/15074

Resources