useEffect triggered after onClick event - with different results - reactjs

I wanted to create a dropdown menu, which shows itself and hides on hovering, and disappears after clicking its item. I thought I found a way to do it - but it works only sometimes. (Or maybe it doesn't work - but sometimes it does.) Details below:
I gotta DropdownMenu2 component, which display is being toggled by onMouseEnter/Leave events. This component (my dropdown menu) holds inside <NavLink> menu items.
I wanted the dropdown menu to disappear after clicking on menu item, so inside <Navlink> I created onClick event which triggers handleClick. This functions sets a click variable - to a CSS className with display:none. click is then passed to <div> that contains the Dropdown menu.
To toggle the dropdown menu display again on mouse hover, I had to get rid of the click class from the div. For that I created useEffect hook, with click dependency - so it fires every time click state changes. And function inside this hook - changes click value, so it no longer represents the CSS display:none class. So after (2.) - div containing dropdown menu has display:none, disapears, and useEffect erases that - making it hover ready.
problem:
this works only sometimes - sometimes useEffect is triggered so fast after onClick, that the dropdown menu doesn't even drop. ( click changes so fast that div container gets the "erasing" class immediately after display:none class )
NaviMainButtonDrop2
import DropdownMenu2 from "./DropdownMenu2";
import useHoverButton from "./sub-components/useHoverButton";
const NaviMainButtonDrop2 = () => {
const { disp, hoverOn, hoverOff } = useHoverButton();
return (
<li
className={`nav-main__button dropdown-us`}
>
<a
className="hover-pointer"
onMouseEnter={hoverOn}
onMouseLeave={hoverOff}
>
title
</a>
{ disp && <DropdownMenu2 /> }
</li>
)
}
export default NaviMainButtonDrop2
useHoverButton (custom hook for NaviMainButtonDrop2)
import { useState } from "react";
const useHoverButton = () => {
const [disp, setDisp] = useState(false);
const hoverOn = () => setDisp(true)
const hoverOff = () => setDisp(false)
return { disp, hoverOn, hoverOff }
}
export default useHoverButton
DropdownMenu2
import "./DropdownMenu.css"
import { NavLink } from "react-router-dom";
import { MenuItemContentSchool } from "./sub-components/MenuItemContentSchool"
import { useEffect } from "react";
import useAddClass from "./sub-components/useAddClass";
const DropdownMenu2 = () => {
const { click, setClick, handleClick } = useAddClass("hide-menu");
useEffect(() => {
console.log("[usEffect]")
setClick("");
}, [click]);
return (
<div className={`dropdown-holder-us ${click}`}>
{/* here menu unfolds */}
{MenuItemContentSchool.map((item) => {
return (
<NavLink
to={item.link}
className={(navData) => (navData.isActive ? "d-content-us active-style" : 'd-content-us')}
onClick={handleClick}
key={item.id}
>
{item.title}
</NavLink>
)
})}
</div>
)
}
export default DropdownMenu2
useAddClass (custom hook for DropdownMenu2)
import { useState } from "react"
const useAddClass = (className) => {
const [click, setClick] = useState("");
const handleClick = () => setClick(className);
return { click , handleClick }
}
export default useAddClass

I think the issue here is that you are not able to get the latest state whenever you update the next state that is why it works sometimes and sometimes it doesn't.
According to me there could be 2 solutions to this, either use a setTimeout or get the latest state when setting the state.
setTimeout solution-
useEffect(() => {
setTimeout(() => {
setClick("")
},2000)
Try and always get the latest state when you update the next state.
useEffect(() => {
console.log("[usEffect]")
setClick((clickLatest) => "");
}, [click]);
and
const handleClick = () => setClick((clickLatest) => className);
This callback will help the useState wait for the latest state and then update the state further.

I think I just found a simple solution to this. I don't understand why useEffect seems to work in a random timing, but using setTimeOut inside it, and delaying the execution of setClick - seems to do the job.
useEffect(() => {
setTimeout(() => {
setClick("")
},2000)

Related

React (Next.js) hook useOutsideClick cancels my links

i'm using the following hook to handle "click away" feature to show/hide a dropdown:
const useOutsideClick = (ref: NonNullable<RefObject<HTMLButtonElement>>) => {
const [outsideClick, setOutsideClick] = useState<boolean | null>(null)
useEffect(() => {
const handleClickOutside = (e: React.MouseEvent | Event) => {
if (
ref &&
!(ref?.current as unknown as RequiredCurrentRef).contains(
e?.target as Node
)
) {
setOutsideClick(true)
} else {
setOutsideClick(false)
}
setOutsideClick(null)
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [ref])
return outsideClick
}
export default useOutsideClick
the hook works fine but once i click on <a href> links (separated component from the dropdown) it does not redirect, so links don't work
how do i solve this?
Edit i'm using bulma.css for dropdowns
I think you dont need to create an extra hook at all.
If you want do du something if the user clicks on or out of the element you can use the onBlur and onFocus callbacks.
If you want to blur it for some other reason (like on the click of a button) you can use the reference of the anchor and call the blur() method whenever you like.

How to change the state when going to previous or next page with React?

I have a state to keep track when a Sub Menu is opened, and toggle it when another Menu Item is clicked. It goes like this:
const [open, setOpen] = useState();
const toggle = (id) => setOpen(id);
onClick={() => toggle(item.navId)}
So, every time I click on a Menu Item, it closes/opens the Sub Menu. But when I click the browser´s buttons to go to the previous page or the next page, the current opened Sub Menu remains opened and it doesn´t open the Sub Menu that was redirected nor close the current opened Sub Menu. What could I use, just like onClick to change the state, when I navigate through the pages with the previous/next buttons in the browser? Thanks!
If you are using react router, you could use the useLocation hook and useEffect to handle closing the menu on route change
const {pathname} = useLocation();
useEffect(()=>{
setOpen(//whatever here)
},[pathname])
I found a similar stack post here and I believe someone's answer might help you with a little modification
import React from "react";
import useHistory from "react-router-dom";
import { SubMenu, OtherComponent } from './'; // import your actual components here
export default function YourComponent() {
const [open, setOpen] = React.useState(false); // toggle submenu
const [locationKeys, setLocationKeys] = React.useState([]);
const history = useHistory();
React.useEffect(() => {
return history.listen((location) => {
if (history.action === "PUSH") {
if (location.key) setLocationKeys([location.key]);
}
if (history.action === "POP") {
if (locationKeys[1] === location.key) {
setLocationKeys(([_, ...keys]) => keys);
// Handle forward event
console.log("forward button");
setOpen(!open); // toggle submenu
} else {
setLocationKeys((keys) => [location.key, ...keys]);
// Handle back event
console.log("back button");
setOpen(!open); // toggle submenu
}
}
});
}, [locationKeys, history]);
return (
<div>
{open ?? <SubMenu/>}
// or
{open ? <SubMenu/> : <OtherComponent/>}
</div>
);
}

How can I test a click event which changes a useState state with enzyme?

I have the following component:
import React, { useState } from "react";
import { Button, ThirteenBold } from "#selfdecode/sd-component-library";
import { PlayIcon } from "assets/icons";
import { TourButtonProps } from "./interfaces";
import { WelcomeVideoModal } from "../../modals/welcome-video-modal";
/**
* The tour button.
*/
export const TourButton: React.FC<TourButtonProps> = (props) => {
const [isIntroVideoShowing, setIsIntroVideoShowing] = useState(false);
return (
<>
<WelcomeVideoModal
isOpen={isIntroVideoShowing}
onClickX={() => setIsIntroVideoShowing(false)}
data-test="tour-button-welcome-video"
/>
<Button
{...props}
width={["max-content"]}
variant="tour"
onClick={() => setIsIntroVideoShowing(true)}
data-test="tour-button"
>
<ThirteenBold
mr={["12px"]}
color="cl_blue"
width={["max-content"]}
letterSpacing={["1px"]}
display={["none", "block"]}
textTransform="uppercase"
>
welcome tour
</ThirteenBold>
<PlayIcon style={{ height: "30px", fill: "#4568F9" }} />
</Button>
</>
);
};
And the test coverage report is complaining that I am not testing both of the onClick events, which change the state.
I've tried two approaches, and both fail.
Approach one was to mock the useState and see if it gets called as I'd have expected it.
This was the test I tried:
const setState = jest.fn();
const useStateMock: any = (initState: any) => [initState, setState];
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
expect(button).toHaveLength(1);
button.simulate("click");
expect(setState).toHaveBeenCalled();
This shouldn't even be the final test, as it doesn't check what was the valuee it was called with, but still, it failed because useState wasn't even called.
The second approach I've tried was to check the prop value on this component:
<WelcomeVideoModal
isOpen={isIntroVideoShowing}
onClickX={() => setIsIntroVideoShowing(false)}
data-test="tour-button-welcome-video"
/>
And this is the test I've tried
test("Check the isIntroVideoShowing changes to true on buton click", () => {
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
const welcomeVideo = wrapper.find(
`[data-test="tour-button-welcome-video"]`
);
expect(button).toHaveLength(1);
expect(welcomeVideo.prop("isOpen")).toEqual(false);
button.simulate("click");
expect(welcomeVideo.prop("isOpen")).toEqual(true);
});
This one failed claiming it was called with false even after the click.
Is there a way to make these work? Or a different approach to cover these?
You need to give wrapper.update for updating the template with state changes after simulating the click event.
test("Check the isIntroVideoShowing changes to true on buton click", () => {
jest.spyOn(React, "useState").mockImplementation(useStateMock);
const button = wrapper.find(`[data-test="tour-button"]`);
const welcomeVideo = wrapper.find(
`[data-test="tour-button-welcome-video"]`
);
expect(button).toHaveLength(1);
expect(welcomeVideo.prop("isOpen")).toEqual(false);
button.simulate("click");
wrapper.update();
expect(welcomeVideo.prop("isOpen")).toEqual(true);
});
Reference - https://enzymejs.github.io/enzyme/docs/api/ShallowWrapper/update.html

Tooltip delay on hover with RXJS

I'm trying to add tooltip delay (300msemphasized text) using rxjs (without setTimeout()). My goal is to have this logic inside of TooltipPopover component which will be later be reused and delay will be passed (if needed) as a prop.
I'm not sure how can I add "delay" logic inside of TooltipPopover component using rxjs?
Portal.js
const Portal = ({ children }) => {
const mount = document.getElementById("portal-root");
const el = document.createElement("div");
useEffect(() => {
mount.appendChild(el);
return () => mount.removeChild(el);
}, [el, mount]);
return createPortal(children, el);
};
export default Portal;
TooltipPopover.js
import React from "react";
const TooltipPopover = ({ delay??? }) => {
return (
<div className="ant-popover-title">Title</div>
<div className="ant-popover-inner-content">{children}</div>
);
};
App.js
const App = () => {
return (
<Portal>
<TooltipPopover>
<div>
Content...
</div>
</TooltipPopover>
</Portal>
);
};
Then, I'm rendering TooltipPopover in different places:
ReactDOM.render(<TooltipPopover delay={1000}>
<SomeChildComponent/>
</TooltipPopover>, rootEl)
Here would be my approach:
mouseenter$.pipe(
// by default, the tooltip is not shown
startWith(CLOSE_TOOLTIP),
switchMap(
() => concat(timer(300), NEVER).pipe(
mapTo(SHOW_TOOLTIP),
takeUntil(mouseleave$),
endWith(CLOSE_TOOLTIP),
),
),
distinctUntilChanged(),
)
I'm not very familiar with best practices in React with RxJS, but this would be my reasoning. So, the flow would be this:
on mouseenter$, start the timer. concat(timer(300), NEVER) is used because although after 300ms the tooltip should be shown, we only want to hide it when mouseleave$ emits.
after 300ms, the tooltip is shown and will be closed mouseleave$
if mouseleave$ emits before 300ms pass, the CLOSE_TOOLTIP will emit, but you could avoid(I think) unnecessary re-renders with the help of distinctUntilChanged

Why useState in React Hook not update state

When i try example from React Hook, i get a problem about useState.
In code below, when click button, i add event for document and check value of count.
My expect is get count in console.log and view as the same. But actual, i got old value (init value) in console & new value in view . I can not understand why count in view changed and count in callback of event not change.
One more thing, when i use setCount(10); (fix a number). And click button many time (>2), then click outside, i got only 2 log from checkCount. Is it React watch count not change then don't addEventListener in next time.
import React, { useState } from "react";
function Example() {
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
console.log("value set is ", count);
document.addEventListener("click", checkCount);
};
const checkCount = () => {
console.log(count);
};
return (
<div>
<p>You clicked {count} times</p>
<p>Click button first then click outside button and see console</p>
<button onClick={() => add()}>Click me</button>
</div>
);
}
export default Example;
If you want to capture events outside of your component using document.addEventListener, you will want to use the useEffect hook to add the event, you can then use the useState to determine if your capturing or not.
Notice in the useEffect I'm passing [capture], this will make it so the useEffect will get called when this changes, a simple check for this capture boolean determines if we add the event or not.
By using useEffect, we also avoid any memory leaks, this also copes with when your unmount the component, it knows to remove the event too.
const {useState, useEffect} = React;
function Test() {
const [capture, setCapture] = useState(false);
const [clickInfo, setClickInfo] = useState("Not yet");
function outsideClick() {
setClickInfo(Date.now().toString());
}
useEffect(() => {
if (capture) {
document.addEventListener("click", outsideClick);
return () => {
document.removeEventListener("click", outsideClick);
}
}
}, [capture]);
return <div>
<p>
Click start capture, then click anywhere, and then click stop capture, and click anywhere.</p>
<p>{capture ? "Capturing" : "Not capturing"}</p>
<p>Clicked outside: {clickInfo}</p>
<button onClick={() => setCapture(true)}>
Start Capture
</button>
<button onClick={() => setCapture(false)}>
Stop Capture
</button>
</div>
}
ReactDOM.render(<React.Fragment>
<Test/>
</React.Fragment>, document.querySelector('#mount'));
p { user-select: none }
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="mount"></div>
#Keith i understand your example but when i apply get some confuse. In origin, i always call function is handleClick and still call it after run handleClickOutside but now i don't know how to apply that with hook.
This is my code that i want insted of Hook
class ClickOutSide extends Component {
constructor(props) {
super(props)
this.wrapperRef = React.createRef();
this.state = {
active: false
}
}
handleClick = () => {
if(!this.state.active) {
document.addEventListener("click", this.handleClickOut);
document.addEventListener("contextmenu", this.handleClickOut);
this.props.clickInside();
} else {
document.removeEventListener("click", this.handleClickOut);
document.removeEventListener("contextmenu", this.handleClickOut);
}
this.setState(prevState => ({
active: !prevState.active,
}));
};
handleClickOut = event => {
const { target } = event;
if (!this.wrapperRef.current.contains(target)) {
this.props.clickOutside();
}
this.handleClick()
}
render(){
return (
<div
onDoubleClick={this.props.onDoubleClick}
onContextMenu={this.handleClick}
onClick={this.handleClick}
ref={this.wrapperRef}
>
{this.props.children}
</div>
)
}
}
export default ClickOutSide

Resources