I know useEffect allows you to run a function after state is updated.
However, I want to run different logic after a state change based on which different event handler causes a state change.
Context
I have a Parent component that shows or hides a child DialogModal component based on the [isDialogShown, setIsDialogShown] = useState(false) in Parent.
When isDialogShown
The Parent passes setIsDialogShown and 2 event handler callbacks to DialogModal: onDismiss (which adds focus to some element) and onConfirm (which adds focus to another element).
When onDismiss or onConfirm on the DialogModal is pressed, setIsDialogShown(false) should run first to hide the DialogModal, then run the respective callbacks to focus on differing elements of the page.
const Parent = () => {
const [isDialogShown, setIsDialogShown] = useState(false);
// These need to run after Dialog is closed.
// In other words, after isDialogShown state is updated to false.
const focusOnElementA = () => { .... };
const focusOnElementB = () => { .... };
const handleDismiss = () => {
setIsDialogShown(false);
focusOnElementA() // Needs to run after state has changed to close the modal
}
const handleConfirm = () => {
setIsDialogShown(false);
focusOnElementB() // Needs to run after state has changed to close the modal
}
return (
<>
<Button onClick={() => { setIsDialogShown(true) }>Open dialog</Button>
<DialogModal
isOpen={isDialogShown}
onDismiss={handleDismiss}
onConfirm={handleConfirm}
/>
</>
)
}
What's the right pattern for dealing with this scenario?
I would use a separate state for the elements A and B to trigger them by in an additional effect. Enqueueing the toggle A/B state ensures the effect handles the update to call the focus A/B handles on the next render after the modal has closed.
const Parent = () => {
const [isDialogShown, setIsDialogShown] = useState(false);
const [toggleA, setToggleA] = useState(false);
const [toggleB, setToggleB] = useState(false);
useEffect(() => {
if (toggleA) {
focusOnElementA();
setToggleA(false);
}
if (toggleB) {
focusOnElementB();
setToggleB(false);
}
}, [toggleA, toggleB]);
const focusOnElementA = () => { .... };
const focusOnElementB = () => { .... };
const handleDismiss = () => {
setIsDialogShown(false);
setToggleA(true);
}
const handleConfirm = () => {
setIsDialogShown(false);
setToggleB();
}
return (
<>
<Button onClick={() => { setIsDialogShown(true) }>Open dialog</Button>
<DialogModal
isOpen={isDialogShown}
onDismiss={handleDismiss}
onConfirm={handleConfirm}
/>
</>
)
}
A slight difference to Drew's answer but achieved using the same tools (useEffect).
// Constants for dialog state
const DIALOG_CLOSED = 0;
const DIALOG_OPEN = 1;
const DIALOG_CONFIRM = 2;
const DIALOG_CANCELLED = 3;
const Parent = () => {
// useState to keep track of dialog state
const [dialogState, setDialogState] = useState(DIALOG_CLOSED);
// Set dialog state to cancelled when dismissing.
const handleDismiss = () => {
setDialogState(DIALOG_CANCELLED);
}
// set dialog state to confirm when confirming.
const handleConfirm = () => {
setDialogState(DIALOG_CONFIRM);
}
// useEffect that triggers on dialog state change.
useEffect(() => {
// run code when confirm was selected and dialog is closed.
if (dialogState === DIALOG_CONFIRM) {
const focusOnElementB = () => { .... };
focusOnElementB()
}
// run code when cancel was selected and dialog is closed.
if (dialogState === DIALOG_CANCELLED) {
const focusOnElementA = () => { .... };
focusOnElementA()
}
}, [dialogState])
return (
<>
<Button onClick={() => { setDialogState(DIALOG_OPEN) }}>Open dialog</Button>
<DialogModal
isOpen={dialogState === DIALOG_OPEN}
onDismiss={handleDismiss}
onConfirm={handleConfirm}
/>
</>
)
}
You should add another state for which element was triggered and then trigger the effect when the states change:
const [action, setAction] = useState('');
// ...code
const handleDismiss = () => {
setAction('dismiss');
setIsDialogShown(false);
}
const handleConfirm = () => {
setAction('confirm');
setIsDialogShown(false);
}
// Add dependencies to useEffect and it will run only when the states change
useEffect(() => {
if(!isDialogShown) {
if(action === 'dismiss') {
focusOnElementA()
} else {
focusOnElementB()
}
}
}, [action, isDialogShown])
Related
I've use this method : Detect click outside React component
The problem is :
I have a component for a burger menu that as a state.
State determine if menu should be open or not.
The 2 components are not in the same container.
When I click burger, I just toggle state (!state).
When I click outside the menu I say state = false
But when I click the burger it now says (!state AND state = false) which cause the menu to stay open when clicking again to (try to) close it.
Burger icon code :
const onClick = () => {
setDeployMode(!deployMode);
dispatch(deployModeAsideMenu({ deployMode: !deployMode }));
};
<HamburgerButton
open={asideMenu.deployMode}
width={28.88}
height={25.27 / 1.5}
strokeWidth={3}
color={asideMenu.deployMode ? "var(--black)" : "var(--pink)"}
animationDuration={0.5}
onClick={onClick}
/>
Aside menu as a ref wrapper :
const [deployMode, setDeployMode] = useState(false);
const changeDeployMode = () => {
setDeployMode(!deployMode);
dispatch(deployModeAsideMenu({ deployMode: !deployMode }));
};
useEffect(() => {
setDeployMode(asideMenu.deployMode);
setLandscapeMode(asideMenu.landscapeMode);
}, [asideMenu]);
const useOutsideAlerter = ref => {
useEffect(() => {
const handleClickOutside = event => {
if (ref.current && !ref.current.contains(event.target) && window.innerWidth < 900) {
setDeployMode(false);
dispatch(deployModeAsideMenu({ deployMode: false }));
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
};
const wrapperRef = useRef(null);
useOutsideAlerter(wrapperRef);
// asideMenu comp
<AsideMenuContainer
ref={wrapperRef}>
// as other code here
<AsideMenuContainer />
I want to call child function from parent and set state of a child's hook ,but I cant able to success it ,simple code is below ,setstate isnt working inside useImperativeHandle.Any help is appreciated ,thx..
const child = forwardRef((props,ref) => {
const [pagerTotalCount, setPagerTotalCount] = useState(0);
const [customerData, setCustomerData] = useState([]);
useImperativeHandle(ref, () => ({
childFunction1: updatePagerTotalCount;
}));
})
const updatePagerTotalCount = (param) => {
setPagerTotalCount(param); // this IS working now...
const pagerInputModel = {
"pageNumber": 1,
"pageSize": 3,
};
myservice.listCustomerList(pagerInputModel).then((res) => {
const { isSuccess, data: customers} = res;
if (isSuccess) {
console.log("api result:" + JSON.stringify(customers)); // this IS working,api IS working
setCustomerData(customers); // this IS NOT working , cant SET this.
console.log("hook result:" + JSON.stringify(customerData)); //EMPTY result.I tested this WITH another buttonclick even IN ORDER TO wait FOR async,but still NOT working
}
});
};
const parent= () => {
const childRef = React.useRef(null)
const handleClick = () => {
childRef.current.childFunction1(11); //sending integer param to child
};
RETURN(
<>
<Button variant="contained" endIcon={<FilterAltIcon />} onClick={handleClick}>
Filtrele
</Button>
<child ref={childRef}/>
</>
)
}
You should define a function to update the state and return that function via useImperativeHandle.
const updatePagerTotalCount = (param) => {
setPagerTotalCount(param);
};
useImperativeHandle(ref, () => ({
childFunction1: updatePagerTotalCount;
}));
Now with above when childRef.current.childFunction1(11); is invoked via parent component, you can see the state is being set correctly.
I need to detect if handleSelectProduct is being called in another component.
My problem is that if I want the child component(ProductDetailsComponent) to rerender, it still outputs the console.log('HELO'). I only want to output the console.log('HELO') IF handleSelectProduct is being click only.
const ProductComponent = () => {
const [triggered, setTriggered] = React.useState(0);
const handleSelectProduct = (event) => {
setTriggered(c => c + 1);
};
return (
<div>
Parent
<button type="button" onClick={handleSelectProduct}>
Trigger?
</button>
<ProductDetailsComponent triggered={triggered} />
</div>
);
};
const ProductDetailsComponent = ({ triggered }) => {
React.useEffect(() => {
if (triggered) {
console.log('HELO');
}
}, [triggered]);
return <div>Child</div>;
};
ReactDOM.render(
<ProductComponent />,
document.getElementById("root")
);
The simplest solution sounds to me by using an useRef to keep the old value, thus consider the console.log only when the triggered value changes.
const ProductDetailsComponent = ({ triggered }) => {
const oldTriggerRef = React.useRef(0);
React.useEffect(() => {
if (triggered !== oldTriggerRef.current) {
oldTriggerRef.current = triggered;
console.log('HELO');
}
}, [triggered]);
return <div>Child</div>;
};
I'm using react-hook-form library with a multi-step-form
I tried getValues() in useEffect to update a state while changing tab ( without submit ) and it returned {}
useEffect(() => {
return () => {
const values = getValues();
setCount(values.count);
};
}, []);
It worked in next js dev, but returns {} in production
codesandbox Link : https://codesandbox.io/s/quirky-colden-tc5ft?file=/src/App.js
Details:
The form requirement is to switch between tabs and change different parameters
and finally display results in a results tab. user can toggle between any tab and check back result tab anytime.
Implementation Example :
I used context provider and custom hook to wrap setting data state.
const SomeContext = createContext();
const useSome = () => {
return useContext(SomeContext);
};
const SomeProvider = ({ children }) => {
const [count, setCount] = useState(0);
const values = {
setCount,
count
};
return <SomeContext.Provider value={values}>{children}</SomeContext.Provider>;
};
Wrote form component like this ( each tab is a form ) and wrote the logic to update state upon componentWillUnmount.
as i found it working in next dev, i deployed it
const FormComponent = () => {
const { count, setCount } = useSome();
const { register, getValues } = useForm({
defaultValues: { count }
});
useEffect(() => {
return () => {
const values = getValues(); // returns {} in production
setCount(values.count);
};
}, []);
return (
<form>
<input type="number" name={count} ref={register} />
</form>
);
};
const DisplayComponent = () => {
const { count } = useSome();
return <div>{count}</div>;
};
Finally a tab switching component & tab switch logic within ( simplified below )
const App = () => {
const [edit, setEdit] = useState(true);
return (
<SomeProvider>
<div
onClick={() => {
setEdit(!edit);
}}
>
Click to {edit ? "Display" : "Edit"}
</div>
{edit ? <FormComponent /> : <DisplayComponent />}
</SomeProvider>
);
}
I tried adding the condition on mouseenter and mouseleave however the modal is not working but when I tried to create a button onClick={() => {openModal();}} the modal will show up. Can you please tell me what's wrong on my code and which part.
const openModal = event => {
if (event) event.preventDefault();
setShowModal(true);
};
const closeModal = event => {
if (event) event.preventDefault();
setShowModal(false);
};
function useHover() {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
return () => {
if (ref.current.addEventListener('mouseenter', enter)) {
openModal();
} else if (ref.current.addEventListener('mouseleave', leave)) {
closeModal();
}
};
}, [ref]);
return [ref, hovered];
}
const [ref, hovered] = useHover();
<div className="hover-me" ref={ref}>hover me</div>
{hovered && (
<Modal active={showModal} closeModal={closeModal} className="dropzone-modal">
<div>content here</div>
</Modal>
)}
building on Drew Reese's answer, you can cache the node reference inside the useEffect closure itself, and it simplifies things a bit. You can read more about closures in this stackoverflow thread.
const useHover = () => {
const ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
const el = ref.current; // cache external ref value for cleanup use
if (el) {
el.addEventListener("mouseenter", enter);
el.addEventListener("mouseleave", leave);
return () => {
el.removeEventLisener("mouseenter", enter);
el.removeEventLisener("mouseleave", leave);
};
}
}, []);
return [ref, hovered];
};
I almost gave up and passed on this but it was an interesting problem.
Issues:
The first main issue is with the useEffect hook of your useHover hook, it needs to add/remove both event listeners at the same time, when the ref's current component mounts and unmounts. The key part is the hook needs to cache the current ref within the effect hook in order for the cleanup function to correctly function.
The second issue is you aren't removing the listener in the returned effect hook cleanup function.
The third issue is that EventTarget.addEventListener() returns undefined, which is a falsey value, thus your hook never calls modalOpen or modalClose
The last issue is with the modal open/close state/callbacks being coupled to the useHover hook's implementation. (this is fine, but with this level of coupling you may as well just put the hook logic directly in the parent component, completely defeating the point of factoring it out into a reusable hook!)
Solution
Here's what I was able to get working:
const useHover = () => {
const ref = useRef();
const _ref = useRef();
const [hovered, setHovered] = useState(false);
const enter = () => setHovered(true);
const leave = () => setHovered(false);
useEffect(() => {
if (ref.current) {
_ref.current = ref.current; // cache external ref value for cleanup use
ref.current.addEventListener("mouseenter", enter);
ref.current.addEventListener("mouseleave", leave);
}
return () => {
if (_ref.current) {
_ref.current.removeEventLisener("mouseenter", enter);
_ref.current.removeEventLisener("mouseleave", leave);
}
};
}, []);
return [ref, hovered];
};
Note: using this with a modal appears to have interaction issues as I suspected, but perhaps your modal works better.