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
Related
I was reading how to use react hooks, callback, and memo adequately. It was mentioned on one of the post that a bad implementation will cost more vs not using useMemo
We started implementing our code to exactly separate the code for UI logic and for the view only
for example, we created a custom hook that handles the UI logic
controller
const useController = () {
const [click, setClick] = useState(0)
const refresh = () => {
setClick(0)
}
const onClick = () => {
setClick(click + 1)
}
return {
click, onClick, resfresh
}
}
component
const Component = () => {
const {click, onClick, resfresh} = useController()
return(
<div>
<label onClick={onClick}>{click}</label>
<button onClick={refresh}>refresh</button>
</div>
)
}
My question here is whether the functions inside the hook are as well recreated when the component is re-rendered. If so, should it be ok to wrap a function inside a custom hook with useCallback or useMemo if there's an actual need?
Yes, you're creating new handler functions every time Component rerenders - but if the only children of the component are built-in JSX elements, it'll have no noticeable effect at all.
If you wanted to return a stable reference to the functions - which would be a good practice but not necessary - yes, it'd be perfectly fine (and expected) to use useCallback or useMemo.
const useController = () {
const [click, setClick] = useState(0);
const refresh = useCallback(() => {
setClick(0);
}, []);
const onClick = useCallback(() => {
setClick(click => click + 1);
}, []);
return {
click, onClick, refresh // make sure to spell this right
};
};
Whether useMemo or useCallback are used would matter more if you had other React components who were children of the Component component use them.
const Component = () => {
const {click, onClick, resfresh} = useController();
return(
<SomeOtherComponent {...{ click, onClick, refresh }} /> // make sure to spell this right
);
};
Making sure the references are as stable as possible can make thing easier performance-wise (...in unusual situations) and can also make their uses easier, especially in conjunction with the common exhaustive-deps linting rule.
I'm experiencing something similar to this:
Should Custom React Hooks Cause Re-Renders of Dependent Components?
So, I have something like this:
const ComponentA = props => {
const returnedValue = useMyHook();
}
And I know for a fact that returnedValue is not changing (prev. returnedValue is === to the re-rendered returnedValue), but the logic inside of the useMyHook does cause internal re-renders and as a result, I get a re-render in the ComponentA as well.
I do realize that this is intentional behavior, but what are my best options here? I have full control over useMyHook and returnedValue. I tried everything as I see it, caching(with useMemo and useCallback) inside of useMyHook on returned value etc.
Update
Just to be more clear about what I'm trying to achieve:
I want to use internal useState / useEffect etc inside of useMyHook and not cause re-render in the ComponentA
Try to find out the reason of re-rendering in your hook, probably caused due to a state update. If it is due to updation of states in useEffect then give the states and values as dependency. If your states are updating in a function, do try to call the function in order to invoke.
If these doesn't work, try to use ref. useRef hook can be used to prevent such re-renders as it will provide you with .current values.
It would be better if you remove your useEffect dependencies one by one so that you can know what dependency is causing the error and create a ref for the same.
You can share a codesandbox and I would be happy to help you out with it.
I want to use internal useState / useEffect etc inside of useMyHook and not cause re-render in the ComponentA
If you want to avoid the renders triggered by useState() or useEffect() then they are not the right tools.
If you need to hold some mutable state without triggering renders then consider using useRef() instead.
This question has a nice comparison of the differences between useState() and useRef().
Here is a simple example:
const ComponentState = () => {
const [value, setValue] = useState(0);
return (<>
<button onClick={() => {
// Will trigger render
setValue(Math.random());
}></button>
</>);
}
const ComponentRef = () => {
const valueRef = useRef(0);
return (<>
<button onClick={() => {
// Will not trigger render
valueRef.current = Math.random();
}></button>
</>);
}
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.
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
Im learning React and using the new implemented "Hooks" from Documentation. I got a problem with a Modal (Dialog from Material UI) and the open/close function using useEffect() function.
I have already read these both articles: React.useState does not reload state from props and How to sync props to state using React hook : setState()
It's already helped me, I have forgot to use the useEffect() function instead I was just setting the useState from what comes from props. Learned that useState will be executed only one time for setting the initial state. But I have however more one problem.
function AddItemModal(props) {
const [open, setOpen] = useState(props.isOpen);
useEffect(() => {
setOpen(props.isOpen);
}, [props.isOpen]);
function handleClose() {
setOpen(false);
}
That works for the first time when I click at the add button (in another Component) and handle the click (it changes the state to true) and I pass it as props to the Modal. But when I click (in modal) at close and try to click at add to open it again, nothing happens. In case needed, here's the code from the component where I handle the click and call the modal.
function ItemsTable() {
const [addModalOpen, setAddModalOpen] = React.useState(false);
const handleAddClick = () => {
setAddModalOpen(true);
};
<Box mt={4} position="fixed" bottom={10} right={10}>
<Fab color="secondary" aria-label="Add" onClick={handleAddClick}>
<AddIcon />
</Fab>
</Box>
<AddItemModal isOpen={addModalOpen} />
You have split your modal state across two components which is confusing and going to lead to bugs like this. You should put the state in one place and update it where necessary. In this case, you could keep the modal state in the ItemsTable and pass in a handler for the modal to access.
Something like:
function ItemsTable() {
const [addModalOpen, setAddModalOpen] = React.useState(false);
const handleAddClick = () => {
setAddModalOpen(true);
};
const handleClose = ()=>{
setAddModalOpen(false)
}
<Box mt={4} position="fixed" bottom={10} right={10}>
<Fab color="secondary" aria-label="Add" onClick={handleAddClick}>
<AddIcon />
</Fab>
</Box>
<AddItemModal isOpen={addModalOpen} handleClose={handleClose}/>
I am not sure if I understood exactly what are you trying to do, but I see that useEffect does not use the state. In order useEffect to be called more than one times, you need to pass the state to it, so your [props.isOpen] needs to change to [open]