I am trying to implement a custom hook to close my nav menu with any click outside of it following this tutorial - https://www.youtube.com/watch?v=XEQ9nEHYIbw&list=PLZlA0Gpn_vH-aEDXnaFNLsqiJWFpIWV03&index=5&t=119s
However it is not working and I can't understand why it's behaving differently.
I have a button with an onClick function to open the menu. I want the custom hook to close the menu on any click outside it. However when I click the menu button to open the menu, the click events are conflicting - the menu state changes to open and then the custom hook runs as well and closes it again.
I have resolved the issue by passing a ref to the button to the hook as well - but is there a better way? Why is this behaving differently to the tutorial where that isn't required?
import "./styles.css";
import { useRef, useState } from "react"
import useClickOutside from "./useClickOutside"
export default function App() {
const [open, setOpen] = useState(false)
const navRef = useRef()
console.log(open)
useClickOutside(navRef, () => {
if (open) setOpen(false);
});
return (
<div className="App">
<header>
<div className="container">
<h1>Logo</h1>
<button onClick={() => setOpen(true)}>{open ? "X" : "Menu"}</button>
</div>
<nav ref={navRef} className={open ? "" : "hide"}>
Home
About
</nav>
</header>
<h1>Main content..</h1>
</div>
);
// useClickOutside.js
import useEventListener from "./useEventListener"
export default function useClickOutside(ref, cb) {
useEventListener(
"click",
(e) => {
if (ref.current == null || ref.current.contains(e.target)) return;
cb(e);
},
document
);
}
// useEventListener.js
import { useEffect, useRef } from "react";
export default function useEventListener(eventType, callback, element = window) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
const handler = (e) => callbackRef.current(e);
element.addEventListener(eventType, handler);
return () => element.removeEventListener(eventType, handler);
}, [eventType, element]);
}
Related
import React, { useEffect } from 'react';
import ReactModal from 'react-modal';
const Modal = () => {
useEffect(() => {
window.scrollTo(0, 0);
}, [])
return (
<ReactModal
>
{children}
</ReactModal>
)
}
I want to scroll to top when react modal is opened. For this I put "window.scrollTo(0, 0)" into useEffect. But when react modal is opened it doesn't work. Why doesn't it work properly? Here is an example:
Try alternative solution with ref:
const Modal = () => {
const divRef = useRef(null);
useEffect(() => {
divRef.current.scrollIntoView({ behavior: "auto" }); // or "smooth" behavior
}, []);
return (
<ReactModal>
<div ref={divRef}>{children}</div> // put the divRef to the place/div you want
</ReactModal>
)
}
I'm using a React hook to track the scroll position on a page. The hook code is as follows:
import { useLayoutEffect, useState } from 'react';
const useScrollPosition = () => {
const [scrollPosition, setScrollPosition] = useState(window.pageYOffset);
useLayoutEffect(() => {
const updatePosition = () => {
setScrollPosition(window.pageYOffset);
};
window.addEventListener('scroll', updatePosition);
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return scrollPosition;
};
export default useScrollPosition;
I then use this in various ways, for example in this component where a class is applied to an element if the page has scrolled more than 10px:
const Component = () => {
const scrollPosition = useScrollPosition();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const newScrolled = scrollPosition > 10;
if (newScrolled !== scrolled) {
setScrolled(newScrolled);
}
}, [scrollPosition]);
return (
<div
className={clsx(style.element, {
[style.elementScrolled]: scrolled,
})}
>
{children}
</div>
);
};
This all works and does what I'm trying to achieve, but the component re-renders continuously on every scroll of the page.
My understanding was that by using a hook to track the scroll position, and by using useState/useEffect to only update my variable "scrolled" in the event that the scroll position passes that 10px threshold, the component shouldn't be re-rendering continuously on scroll.
Is my assumption wrong? Is this behaviour expected? Or can I improve this somehow to prevent unnecessary re-rendering? Thanks
another idea is to have your hook react only if the scroll position is over 10pixel :
import { useEffect, useState, useRef } from 'react';
const useScrollPosition = () => {
const [ is10, setIs10] = useState(false)
useEffect(() => {
const updatePosition = () => {
if (window.pageYOffset > 10) {setIs10(true)}
};
window.addEventListener('scroll', updatePosition);
return () => window.removeEventListener('scroll', updatePosition);
}, []);
return is10;
};
export default useScrollPosition;
import React, { useEffect, useState } from "react";
import useScrollPosition from "./useScrollPosition";
const Test = ({children}) => {
const is10 = useScrollPosition();
useEffect(() => {
if (is10) {
console.log('10')
}
}, [is10]);
return (
<div
className=''
>
{children}
</div>
);
};
export default Test
so your component Test only renders when you reach that 10px threshold, you could even pass that threshold value as a parameter to your hook, just an idea...
Everytime there is useState, there will be a re-render. In your case you could try useRef to store the value instead of useState, as useRef will not trigger a new render
another idea if you want to stick to your early version is a compromise :have the children of your component memoized, say you pass a children named NestedTest :
import React from 'react'
const NestedTest = () => {
console.log('hit nested')
return (
<div>nested</div>
)
}
export default React.memo(NestedTest)
you will see that the 'hit nested' does not show in the console.
But that might not be what you are expecting in the first place. May be you should try utilizing useRef in your hook instead
I am working on a React project in that I have a button, for that button I have written one onClick function now what I need is when I click the button it only needs to change background color only to mobile screen from min(0px) to max(576px) in this screen only the function change has to apply.
This is my code
import React, { useState } from 'react';
import './App.css';
function App() {
const [color,setColor]=useState('red');
const [textColor,setTextColor]=useState('white');
const changeBackGround =() =>{
{setColor("black");setTextColor('red')}
}
return (
<div className="App">
<button style={{background:color,color:textColor}} onClick={changeBackGround} className='btn btn-primary'>Click here</button>
</div>
);
}
export default App
If you have any questions please let me know. Thank you
Have a state object that updates the the className on the button click. Update the className in the css media query.
import React, { useState } from 'react';
import './App.css';
function App() {
const [color,setColor]=useState('red');
const [textColor,setTextColor]=useState('white');
const [buttonClassName, setButtonClassName] = useState("");
const changeBackGround = () =>{
setColor("black");
setTextColor('red');
setButtonClassName("btn-update");
}
return (
<div className="App">
<button
style={{background:color,color:textColor}}
onClick={changeBackGround}
className={`btn btn-primary ${buttonClassName}`}>
Click here
</button>
</div>
);
}
export default App
#media screen and (max-width: 576px) {
.btn-update {
background-color: "green";
}
}
You can do this in 2 ways
check your window.innerWidth . But this will not work when you resize your window in the browser. To Test this what you can do is resize your browser window so the width is less than 576px and refresh your screen and click the button now .
const changeBackGround =() =>{
if(window.innerWidth < 576){
setColor("black");
setTextColor('red')}
} else {
...do something
}
}
Attach an event listener which listens for your resize event , now when you resize the window the width is maintained in state.
function App() {
const [deviceSize, changeDeviceSize] = useState(window.innerWidth);
const [color, setColor] = useState('red');
const [textColor, setTextColor] = useState('white');
useEffect(() => {
const handleResize = () => changeDeviceSize(window.innerWidth);
window.addEventListener('resize', handleResize);
// don't forget to remove the event listener on unmounting the component
return () => window.removeEventListener('resize', handleResize);
}, []);
const changeBackGround = () => {
if (deviceSize < 576) {
{
setColor('black');
setTextColor('red');
}
}
};
return (
<div className="App">
<button
style={{background: color, color: textColor}}
onClick={changeBackGround}
className="btn btn-primary"
>
Click here
</button>
</div>
);
}
If you want to trigger things dynamically, use custom hooks to get window size (generic) and another custom hook to check if it's valid for mobile (can be kept in a separate hooks folder).
useWindowSize.js
// Hook from https://usehooks.com/useWindowSize/
function useWindowSize() {
// Initialize state with undefined width/height so server and client renders match
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined
});
useEffect(() => {
// Handler to call on window resize
function handleResize() {
// Set window width/height to state
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
// Add event listener
window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array ensures that effect is only run on mount
return windowSize;
}
useIsMobile.js
const MAX_SIZE_FOR = { mobile: 576 };
const useIsMobile = () => {
const { width } = useWindowSize();
return width < MAX_SIZE_FOR;
};
yourComponent.js
import React, { useState } from "react";
import { useIsMobile } from './useIsMobile'
import "./App.css";
function App() {
const [style, setStyle] = useState({ background: "red", textColor: "white" });
const isMobile = useIsMobile();
const changeBackGround = () => {
if (isMobile) {
setStyle({ ...style, background: "black", textColor: "red" });
}
};
return (
<div className="App">
<button style={style} onClick={changeBackGround} className="btn btn-primary">
Click here
</button>
</div>
);
}
export default App;
You can even change the color on the fly via useEffect
const { width } = useWindowSize();
useEffect(changeBackGround, [width]);
You can create a state variable to update when the screen gets to a certain width. In a useEffect(), you can add a eventListener to the window that listens to the screen resizing. When the screen gets resized to a certain width, we update the state and use it to do conditional rendering in the return.
const [show, setShow] = useState(false); // state value for showing / hiding
useEffect(() => {
const handleResize = () => {
window.innerWidth < 576 ? setShow(true) : setShow(false); // set hide / show
}
window.addEventListener("resize", handleResize); // add event listener
}, []);
return ({
show ? <h1>show</h1>: <h1>hide</h1>
});
I am newbie in React js, and I have made a toggle class button in Reactjs.
But When I clicked i got error message.
TypeError : Cannot read property 'toggle' of undefined
This is my code below.
What is wrong with my code? please help.
import React from 'react'
export default function About() {
const navRef = React.useRef(null);
const btnRef = React.useRef(null);
const onToggleClick = () => {
navRef.current.classList.toggle("show");
btnRef.current.classList.toggle("active");
};
return (
<>
<button onClick={onToggleClick} ref={btnRef}>Toggle</button>
<nav ref={navRef}>Navigation menu</nav>
</>
);
}
In react, you can learn to toggle by using state.
In the following example, we set a toggle state show, and toggle it true/false.
We will then set the className with the appropriate css class we want it to have.
import React, { useState, useEffect } from 'react'
export default function About() {
const [ show, setShow ] = useState(false) //default to hide
useEffect(() => {
if (show) {
document.body.classList.add('bodyclass');
} else {
document.body.classList.remove('bodyclass');
}
}, [show])
const onToggleClick = () => setShow(!show) //if it's true, set to false. vice-versa
return (
<>
<button onClick={onToggleClick} className={ show ? 'active' : ''}>Toggle</button>
<nav className={ show ? 'show' : ''}>Navigation menu</nav>
</>
);
}
I want to toggle an input element by using custom hook.
Here's my custom hook:
import { RefObject, useEffect } from "react";
export const useEscape = (
ref: RefObject<HTMLElement>,
triggerFn: () => void
) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
triggerFn();
}
};
document.addEventListener("click", handleClickOutside);
return () => window.removeEventListener("click", handleClickOutside);
});
};
and the example that would use the hook
import * as React from "react";
import "./styles.css";
import { useEscape } from "./useEscape";
export default function App() {
const [showInput, setShowInput] = React.useState(false);
const inputRef = React.useRef(null);
useEscape(inputRef, () => {
if (showInput) setShowInput(false);
});
return (
<div>
{showInput && (
<input ref={inputRef} placeholder="click outside to toggle" />
)}
{!showInput && (
<span
style={{ border: "1px solid black" }}
onClick={() => {
console.log("toggle to trigger");
setShowInput(true);
}}
>
click to toggle input
</span>
)}
</div>
);
}
Here's the link to codesandbox demo.
Here's the issue. After I clicked on the span element to toggle into input state. After click outside of the input element, it would never able to toggle back to input state again.
I guess I know why's that happening. The react ref is still pointing to the input element that was created at the first place. Howeve, when react toggle to showing span state, it unmount the input element, and my custom hook never sync with React for the new input element. How can I customize my useEscape hook so the react ref would sync up? (By the way, I want to not use styling as a workaround which visually 'hides' the input element).
import { RefObject, useEffect } from "react";
export const useEscape = (
ref: RefObject<HTMLElement>,
triggerFn: () => void
) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
triggerFn();
}
};
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}, [ref, triggerFn]);
};
Your entire logic is absolutely correct. There is a slight error, instead of
window.removeEventListener, change it to document.removeEventListener.
You are removing event listener on global window object which leads to bug.