Forceful Re-render with useEffect - reactjs

import * as React from "react";
// import "./style.css";
export default function App() {
let [width, setWidth] = React.useState(window.innerWidth);
let [height, setHeight] = React.useState(window.innerHeight);
React.useEffect(() => {
console.log("useEffect is called");
window.addEventListener("resize", () => {
setHeight(window.innerHeight);
setWidth(window.innerWidth);
});
}, []);
return (
<div>
{/* <button onClick={handler}> Submit </button> */}
<h1>
{" "}
{height},{width}{" "}
</h1>
</div>
);
}
The above code causes re-render of height and width values on the UI (height =windows.innerHeight & width = windows.innerWidth) despite using useEffect with an empty dependency array.
I've deployed useState inside useEffect to update height and width. My understanding was that useEffect gets executed only once(after the initial render) if used with an empty dependency array but on resizing the screen size, height and width gets updated as well thereby causing re-render

you should use const to declare your states
window.addEventListener is declared only once but it will be triggered every resize - so your state will be updated every resize
every time your state change the component will rerender

The useEffect is called only once, but since you have added a listener on the window object using addEventListener. The code inside the handler (which sets the state) gets executed, whenever there window size is changed. This state update will cause your component to re-render and update the UI

I think that's because you added addEventListener for resizing window in useEffect, So whenever window in resized it's callback function will be called and your states will be changed so your component will be re-rendered

the window.addEventListener method runs just one time and then you have a listener for resize and its call back excuted. the window.addEventListener does'nt execute on each resize, it's callback does, ist it clear?

Related

Using setTimeout in React functional component in useCallback and useEffect

I have been trying to use setTimeout in react to make a Popper component disappear off of the user screen. The Popper is set to appear after the user clicks a button. The visibility of the Popper component is tied to the "popperOpen" state below.
I have tried putting the setTimeout method inside of the callback function of the relevant button and also in a useEffect with no dependency array (both shown below). In the former, the Popper disappears immediately. In the latter, the Popper never disappears. Not shown below, but I have also tried useEffect with "popperOpen" in the dependency array.
What would be the proper way to get the desired functionality? And more fundamentally, how should I think about setTimeout in the context of react? (i.e., constant re-renders).
setTimeout in useCallback
const shareRef = useRef()
const [popperOpen, setPopperOpen] = useState(false);
const [anchor, setAnchor] = useState(shareRef.current)
useEffect(() => {
setAnchor(shareRef.current);
}, [shareRef])
const onShare = useCallback(() => {
const id = window.location.pathname.split('/')[2]
navigator.clipboard.writeText(window.location.origin + "/share/" + id)
setPopperOpen(true)
console.log("popper open")
const timeout = setTimeout(()=>{setPopperOpen(false)},1000)
console.log(timeout)
return(()=>{clearTimeout(timeout)})
},[setPopperOpen])
return(
<Button startIcon={<BiShare />} onClick={onShare} ref={shareRef}>
Share
</Button>
<Popper anchorEl={anchor} open={popperOpen} placement='bottom'>
Pressed Button!
</Popper>
)
setTimeout in useEffect
const shareRef = useRef()
const [popperOpen, setPopperOpen] = useState(false);
const [anchor, setAnchor] = useState(shareRef.current)
useEffect(() => {
setAnchor(shareRef.current);
}, [shareRef])
const onShare = useCallback(() => {
const id = window.location.pathname.split('/')[2]
navigator.clipboard.writeText(window.location.origin + "/share/" + id)
setPopperOpen(true)
console.log("popper open")
},[setPopperOpen])
useEffect(()=>{
const timeout = setTimeout(()=>{setPopperOpen(false)},1000)
console.log(timeout)
return(()=>{clearTimeout(timeout)})
},[])
return(
<Button startIcon={<BiShare />} onClick={onShare} ref={shareRef}>
Share
</Button>
<Popper anchorEl={anchor} open={popperOpen} placement='bottom'>
Pressed Button!
</Popper>
)
This is a problem I've run into often when trying to use state setters as callbacks to other asynchronous/event-driven functions like addEventListener. My understanding of the problem is that due to the nested functions, a closure is being created around the initial value of setPopperOpen, so when that value is invoked later on, it is no longer equal to the latest setPopperOpen (because that variable is reassigned upon each re-render).
My solution has been to create a variable outside of the scope of your component, at the level of the module, for example let _setPopperOpen. Then, inside of your component, set _setPopperOpen = setPopperOpen (not in a useEffect or anything, just at the top level of your component). Finally, inside of your setTimeout, invoke the function _setPopperOpen, rather than setPopperOpen

React: Hook not triggering when parent component changes boolean value passed to child

so I have a parent that is responsible for rendering a list of items. When the item is clicked it will send an event to the parent, that will record on a Map the selection state. If selected it passes the selected state to the child so that it can change style. Here's the code for the parent:
export default function CreateYourMixProductsList(props: ProductsInterface) {
const selectedProducts: Map<number, Product> = new Map<number, Product>();
function onProductClick(product: Product): void {
selectedProducts.set(product.id, product);
}
return (
<>
<List sx={{width: '100%', bgcolor: 'background.paper'}}>
{props?.products?.map(product => (
<ProductListItem key={product.id} product={product}
selected={selectedProducts.has(product.id)}
onClick={(product) => onProductClick(product)} />
))}
</List>
</>
);
and the child
export default function ProductListItem(props: ProductProps) {
const [selected, setSelected] = React.useState(false);
function onClick(product: Product) {
props.onClick(product);
}
useEffect(() => {
setSelected(!selected);
}, [props.selected]);
return (
<>
<ListItemButton alignItems="flex-start" onClick={event => {onClick(props.product)}} selected={props.selected ? selected : false}>
//omitted code to keep it short
The useEffect is triggered only on rendering, whilst to my understanding, it should be triggered every time the props passed down is an immutable variable. What am I missing here?
here's a bit of documentation on useEffect
https://reactjs.org/docs/hooks-effect.html
By default, useEffect will trigger after each render. Adding an array will skip this process and instead only trigger the useEffect on the changing of the specified prop(s). If we take a look at your code example:
useEffect(() => {
setSelected(!selected);
}, [props.selected]);
this block is triggering the useEffect each time props.selected is updated, hence why it only triggers on render, when you give the value of false to selected. What you can do instead is call setSelected(!selected) as part of the onClick event. Once this is occurs, you can update the useEffect to handle any logic that should occur when selected changes, or remove the useEffect entirely.
Lets unwrap this:
Why is the useEffect not running?
Hooks re-run every time a variable in their dependency array changes. Your hook is not running again because the value of props.selected does not change.
You can easily verify this by simply logging the value in the component.
Why is props.selected not changing?
Your click handler correctly sets the value on your Map. However, React does not recognize that a a new value was set inside the map. The component never re-renders and selectedProducts.has() is not called again. So the value of props.selected is indeed still the same.
How can you make React recognize your change?
First of all, you should avoid declaring state like this in your component. Each time this component renders it will re-declare all variables defined inside it (selectedProducts will always be set to a new empty map). Use reacts hook api instead.
To make the variable stick - and reactive - you can simply use it with useState() as you did in your child-component. E.g.:
...
const [selectedProducts, setSelectedProducts] = useState<Map<number, Product>>(new Map<number, Product>());
function onProductClick(product: Product): void {
selectedProducts.set(product.id, product);
// Note how we are re-setting the variable to make React aware of the change!
setSelectedProducts(new Map(selectedProducts));
}
...
It is important to note that Reacts useState hook compares values with tripple-equal (===) to determine whether it should trigger a re-render. Due to the nature of JavaScript (where variables only hold references to objects), setting a new value on a Map will not change its reference. To trigger a re-render you have to replace the Map.
Creating new objects is computationally cheap so this shouldn't cause any issues most of the times.

React: why is that changing the current value of ref from useRef doesn't trigger the useEffect here

I have a question about useRef: if I added ref.current into the dependency list of useEffect, and when I changed the value of ref.current, the callback inside of useEffect won't get triggered.
for example:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Also I know I can use useState here. This is not what I am asking. And also I know that ref stay referentially the same during re-renders so it doesn't change. But I am not doing something like
const myRef = useRef(1);
useEffect(() => {
//...
}, [myRef]);
I am putting the current value in the dep list so that should be changing.
I know I am a little late, but since you don't seem to have accepted any of the other answers I'd figure I'd give it a shot too, maybe this is the one that helps you.
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Short answer, no.
The only things that cause a re-render in React are the following:
A state change within the component (via the useState or useReducer hooks)
A prop change
A parent render (due to 1. 2. or 3.) if the component is not memoized or otherwise referentially the same (see this question and answer for more info on this rabbit hole)
Let's see what happens in the code example you shared:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Initial render
myRef gets set to {current: 1}
The effect callback function gets registered
React elements get rendered
React flushes to the DOM (this is the part where you see the result on the screen)
The effect callback function gets executed, "myRef current changed" gets printed in the console
And that's it. None of the above 3 conditions is satisfied, so no more rerenders.
But what happens when you click the button? You run an effect. This effect changes the current value of the ref object, but does not trigger a change that would cause a rerender (any of either 1. 2. or 3.). You can think of refs as part of an "effect". They do not abide by the lifecycle of React components and they do not affect it either.
If the component was to rerender now (say, due to its parent rerendering), the following would happen:
Normal render
myRef gets set to {current: 1} - Set up of refs only happens on initial render, so the line const myRef = useRef(1); has no further effect.
The effect callback function gets registered
React elements get rendered
React flushes to the DOM if necessary
The previous effect's cleanup function gets executed (here there is none)
The effect callback function gets executed, "myRef current changed" gets printed in the console. If you had a console.log(myRef.current) inside the effect callback, you would now see that the printed value would be 2 (or however many times you have pressed the button between the initial render and this render)
All in all, the only way to trigger a re-render due to a ref change (with the ref being either a value or even a ref to a DOM element) is to use a ref callback (as suggested in this answer) and inside that callback store the ref value to a state provided by useState.
https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
use useCallBack instead, here is the explanation from React docs:
We didn’t choose useRef in this example because an object ref doesn’t
notify us about changes to the current ref value. Using a callback ref
ensures that even if a child component displays the measured node
later (e.g. in response to a click), we still get notified about it in
the parent component and can update the measurements.
Note that we pass [] as a dependency array to useCallback. This
ensures that our ref callback doesn’t change between the re-renders,
and so React won’t call it unnecessarily.
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
Ok so I think what you're missing here is that changing a ref's value doesn't cause a re-render. So if it doesn't cause re-renders, then the function doesn't get run again. Which means useEffect isn't run again. Which means it never gets a chance to compare the values. If you trigger a re-render with a state change you will see that the effect will now get run. So try something like this:
export default function App() {
const [x, setX] = useState();
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<button
onClick={() => {
myRef.current = myRef.current + 1;
// Update state too, to trigger a re-render
setX(Math.random());
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
);
}
Now you can see it will trigger the effect.

Save React Component scroll position in Redux on Component update

I have a React Component using a hook to save the scroll position of the component when the component unmounts. This works great but fails when navigating from one set of data to another set of data without the component unmounting.
For instance, imagine the Slack Interface where there is a sidebar of message channels on the left and on the right is a list of messages (messageList). If you were to navigate between two channels, the messageList component would update with a new set of data for the messageList, but the component was never unmounted so scroll position never gets saved.
I came up with a solution that works, but also throws a warning.
My current useEffect hook for the component (stripped down) and the code that currently saves scroll position whenever the messageList ID changes:
// Component...
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
// Save scroll position when Component unmounts
useEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, []);
// Save scroll position when Parent ID changes
const oldParent = usePrevious(parent);
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
// ...Component
The error this throws is:
Warning: Cannot update a component from inside the function body of a different component.
And the line that is causing it is the setScrollOffset call inside of the last if block. I'm assuming that while this works it is not the way that I should be handling this sort of thing. What is a better way to handle saving scroll position when a specific prop on the component changes?
Add parent._id to the dependency array. Refactor your code to still cache the previous parent id, add that to the dependency, and move the conditional test inside the effect.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect.
// Return previous parent id and cache current
const oldParent = usePrevious(parent);
// Save scroll position when Component unmounts or parent id changes
useEffect(() => {
if (oldParent && parent._id !== oldParent._id) {
setScrollOffset(oldParent._id, list ? list.scrollTop : 0);
}
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id, oldParent]);
If this does't quite fit the bill, then use two effects, one for the mount/unmount and the other for just updates on the parent id.
Thanks to the suggestions of #drew-reese, he got me pointed down the right path. After adopting his solution (which previously I could not get working properly), I was able to isolate my problem to usage with react-router. (connected-react-router in my case). The issue was that the component was rendering and firing the onScroll event handler and overwriting my scroll position before I could read it.
For me the solution ended up being to keep my existing useEffect hook but pull the scroll offset save out of it and into useLayoutEffect (Had to keep useEffect since there is other stuff in useEffect that I removed for the sake of keeping the sample code above lean). useLayoutEffect allowed me to read the current scroll position before the component fired the onScroll event which was ultimately overwriting my saved scroll position reference to 0.
This actually made my code much cleaner overall by removing the need for my usePrevious hook entirely. My useLayoutEffect hook now looks like this:
useLayoutEffect(() => {
return () => {
setScrollOffset(parent._id, scrollPos.current);
};
}, [parent._id]);

How to Unmount React Functional Component?

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

Resources