Why useState in React Hook not update state - reactjs

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

Related

how to register onClick and onDoubleClick event from same JSX element in React Functional Component?

Is there a proper , established way to register either User permormed single click or double click on same JSX element inside of Function component. After reading articles on stackOverflow and watching youtube, the easiest solution for beginner like me was to create custom hook - useClickHook, and to use callback inside setTimeout api. In App component I'm using useEffect hook,
clickHook value is inside of array of dependencies. On first render its 0 , after first click = 1 , if doubleClick = 2; inside of If() statement in useeffect - console.log() reperesents function to be invoked. and after i'm setting clickHook value back to default 0.
Here is what I coded (minimal reproducible example)
import { useState, useEffect } from 'react';
export const useClickHook = (detail) => {
const [clickDetail, setClickDetail] = useState(0);
useEffect(() => {
const timer = setTimeout(() => {
setClickDetail(detail)
}, 200);
return () => {
clearTimeout(timer);
}
}, [detail]);
return clickDetail;
}
import { useEffect, useState } from 'react';
import { useClickHook } from './ClickHook';
function App() {
const [click, setClick] = useState(0);
const clickHook = useClickHook(click);
useEffect(() => {
if (clickHook === 1) {
console.log('single click')
}
if (clickHook === 2) {
console.log('double click')
}
setClick(0);
},[clickHook])
return (
<div className="App">
Hello World
<button
onClick={(e) => {
setClick(e.detail);
}}
onDoubleClick={(e) => {
setClick(e.detail);
}}
>
Click
</button>
</div>
);
}
export default App;
How to improve it?
Will be glad for every suggestions.
Edited!!!
So one way to improve is simple to remove onDoubleClick event handler from button JSX element .
Following code can be safely deleted
onDoubleClick={(e) => {
setClick(e.detail);
}}
Thus , on double click setClick will be called only twice , not 3 times like in example proposed originaly , and the rest will be done by the custom hook as before.
Pretty sure you can already do this in React with no special code required.
Demo here
export default function Demo() {
const handleClick = (event) => {
console.log(event.detail);
switch (event.detail) {
case 1: {
console.log("single click");
break;
}
case 2: {
console.log("double click");
break;
}
default: {
break;
}
}
};
return (
<div>
<div>
<button onClick={handleClick}>Double click</button>
</div>
</div>
);
}

useEffect triggered after onClick event - with different results

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)

Test document listener with React Testing Library

I'm attempting to test a React component similar to the following:
import React, { useState, useEffect, useRef } from "react";
export default function Tooltip({ children }) {
const [open, setOpen] = useState(false);
const wrapperRef = useRef(null);
const handleClickOutside = (event) => {
if (
open &&
wrapperRef.current &&
!wrapperRef.current.contains(event.target)
) {
setOpen(false);
}
};
useEffect(() => {
document.addEventListener("click", handleClickOutside);
return () => {
document.removeEventListener("click", handleClickOutside);
};
});
const className = `tooltip-wrapper${(open && " open") || ""}`;
return (
<span ref={wrapperRef} className={className}>
<button type="button" onClick={() => setOpen(!open)} />
<span>{children}</span>
<br />
<span>DEBUG: className is {className}</span>
</span>
);
}
Clicking on the tooltip button changes the state to open (changing the className), and clicking again outside of the component changes it to closed.
The component works (with appropriate styling), and all of the React Testing Library (with user-event) tests work except for clicking outside.
it("should close the tooltip on click outside", () => {
// Arrange
render(
<div>
<p>outside</p>
<Tooltip>content</Tooltip>
</div>
);
const button = screen.getByRole("button");
userEvent.click(button);
// Temporary assertion - passes
expect(button.parentElement).toHaveClass("open");
// Act
const outside = screen.getByText("outside");
// Gives should be wrapped into act(...) warning otherwise
act(() => {
userEvent.click(outside);
});
// Assert
expect(button.parentElement).not.toHaveClass("open"); // FAILS
});
I don't understand why I had to wrap the click event in act - that's generally not necessary with React Testing Library.
I also don't understand why the final assertion fails. The click handler is called twice, but open is true both times.
There are a bunch of articles about limitations of React synthetic events, but it's not clear to me how to put all of this together.
I finally got it working.
it("should close the tooltip on click outside", async () => {
// Arrange
render(
<div>
<p data-testid="outside">outside</p>
<Tooltip>content</Tooltip>
</div>
);
const button = screen.getByRole("button");
userEvent.click(button);
// Verify initial state
expect(button.parentElement).toHaveClass("open");
const outside = screen.getByTestId("outside");
// Act
userEvent.click(outside);
// Assert
await waitFor(() => expect(button.parentElement).not.toHaveClass("open"));
});
The key seems to be to be sure that all activity completes before the test ends.
Say a test triggers a click event that in turn sets state. Setting state typically causes a rerender, and your test will need to wait for that to occur. Normally you do that by waiting for the new state to be displayed.
In this particular case waitFor was appropriate.

React custom mount hook

I always forget to add empty dependencies array in my useEffect hook in React to run the effect only once. That's why I decided to use a custom hook with a clear intention in it's name to run only once on mount. But I don't understand why is it running twice?
import React from "react";
import "./styles.css";
function useOnMount(f) {
const isMountedRef = React.useRef(false);
console.log("ref1", isMountedRef.current);
if (!isMountedRef.current) {
f();
isMountedRef.current = true;
console.log("ref2", isMountedRef.current);
}
}
export default function App() {
useOnMount(() => {
console.log("useOnMount");
});
return <div>Hello useOnMount</div>;
}
Here's the output:
ref1 false
useOnMount
ref2 true
ref1 false
useOnMount
ref2 true
I use ref hook to keep mutable flag between renders. But I can't understand why isMountedRef.current is true on the first render and reverts to false on the second render 🤔
You keep forgetting the dependencies array? Why not just remember to write it once in your custom hook?
function useOnce (once) {
return useEffect(once, [])
}
Using it in your App -
function App () {
useOnce(_ => console.log("useOnce"))
return <div>hello</div>
}
Here's a demo -
const { useEffect, useState } = React
function useOnce (once) {
return useEffect(once, [])
}
function App () {
useOnce(_ => alert("Wake up. It's time to make candy!"))
const [candy, setCandy] =
useState(0)
const earn =
<button onClick={_ => setCandy(candy + 1)}>
Make candy
</button>
const spend =
<button onClick={_ => setCandy(candy - 10)}>
Buy Chocolate (10)
</button>
return <div>
<b>Candy Box</b>
<p>Alert will only appear one time after component mounts</p>
<p>
{earn}
{(candy >= 10) && spend}
</p>
<p>You have {candy} candies</p>
</div>
}
ReactDOM.render(<App/>, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
This one works for me and doesn't generate react-hooks/exhaustive-deps warnings.
function useOnce (once) {
const [ res ] = useState(once)
return res
}
Usage:
function App () {
useOnce(() => {
console.log("useOnce")
// use any App props or result of other hooks
...
})
return <div>hello</div>
}
It was recommended here for another task but it works well as a componentDidMount replacement. Any parameters may be used inside the function. Just make sure you really don't need to monitor changes of props.

How to call a function onClick which is defined in React Hooks useEffect()?

useEffect(() => {
//Calling this function
const handleClick = () => {
const lazyApp = import("firebase/app")
const lazyStore = import("firebase/firestore")
//Some code here
}
}, [])
//Calling the function here
<MDBBtn
to=""
className="btn p-0 btn-white bg-transparent"
title="Add To Cart"
onClick={handleClick}
>
<FontAwesomeIcon
icon={faShoppingCart}
style={{ fontSize: "1.3rem" }}
/>
</MDBBtn>
I am trying to call a function after a button click event. But, I want that function to be defined inside useEffect() due to some reasons. I get the error that handleClick is not defined. What is the solution for this?
Because you can't. That's not how it works. useEffect — without a second param— works like componentDidMount lifecycle. The function you're calling has already been called when the component is mounted.
I don't know for what reason you're trying to call an onclick function inside of an useEffect, I've never seen anything like that.
I found this article describing how to implement in a class component:
componentDidMount() {
const lazyApp = import('firebase/app')
const lazyDatabase = import('firebase/database')
Promise.all([lazyApp, lazyDatabase]).then(([firebase]) => {
const database = getFirebase(firebase).database()
// do something with `database` here,
// or store it as an instance variable or in state
// to do stuff with it later
})
}
In your case you will need to mimic that componentDidMount via useEffect and implement some click handler.
You could try to do the following:
let lazyApp, lazyDatabase;
useEffect(() => {
lazyApp = import("firebase/app");
lazyDatabase = import("firebase/firestore");
}, []);
const handleClick = (e) => {
e.preventDefault();
Promise.all([lazyApp, lazyDatabase]).then(([firebase]) => {
const database = getFirebase(firebase).database()
// do something with `database` here,
// or store it as an instance variable or in state
// to do stuff with it later
})
};
This is untested, but maybe that will help.
Even though this seems weird and like an anti-pattern, this is how you could implement it. Using a useRef to keep the reference for your function.
Please check if you really need to do this.
function App() {
console.log("Rendering App...");
const click_ref = React.useRef(null);
React.useEffect(()=>{
function handleClick() {
console.log("Running handleClick defined inside useEffect()...");
}
console.log("Updating click_ref...");
click_ref.current = handleClick;
},[]);
return(
<React.Fragment>
<div>App</div>
<button onClick={()=>click_ref.current()}>Click</button>
</React.Fragment>
);
}
ReactDOM.render(<App/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"/>

Resources