just started to learn Next Js. I wanna hide dropdown when i clicked outside the button. Code works fine in create-react-app. But i tried to implement in nextjs, it doesnt working.
const LanguageRef = useRef();
const [languageDD, setLanguageDD] = useState(false);
console.log(languageDD);
useEffect(() => {
if (!languageDD) return;
const checkIfClickedOutside = (e) => {
if (
languageDD &&
LanguageRef.current &&
!LanguageRef.current.contains(e.target)
) {
setLanguageDD(false);
}
};
document.addEventListener("click", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("click", checkIfClickedOutside);
};
}, [languageDD]);
link tag
<a onClick={() => setLanguageDD((prev) => !prev)}>Language </a>
Does useEffect work in Nextjs?
Working Solution:
const LanguageRef = useRef();
const LanguageDDRef = useRef();
const [languageDD, setLanguageDD] = useState(false);
console.log(languageDD);
useEffect(() => {
console.log("useeffect")
if (!languageDD) return;
const checkIfClickedOutside = (e) => {
if (
languageDD &&
LanguageRef.current &&
!LanguageRef.current.contains(e.target) &&
LanguageDDRef.current &&
!LanguageDDRef.current.contains(e.target)
) {
setLanguageDD(false);
}
};
document.addEventListener("click", checkIfClickedOutside);
return () => {
// Cleanup the event listener
document.removeEventListener("click", checkIfClickedOutside);
};
}, [languageDD]);
<a onClick={() => setLanguageDD((prev) => !prev) ref={LanguageDDRef}}>Language </a>
I reworked and also splitted the clickOutside function to a Custom Hook, because it can reuse at the other components. Like this:
import React, { useEffect } from 'react';
export const useClickOutside = (ref, handler) => {
useEffect(() => {
const listener = (e) => {
// Do nothing if clicking ref's element or descendent elements
if (!ref?.current || ref.current.contains(e.target)) {
return;
}
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
};
}, [handler, ref]);
};
Finally, you just need import and use useClickOutside hook, like this:
import React, { useRef, useState } from 'react';
import { useClickOutside } from '../src/hooks';
export default function Home() {
const LanguageDDRef = useRef();
const [languageDD, setLanguageDD] = useState(true);
useClickOutside(LanguageDDRef, () => setLanguageDD(false));
const handleToggleButton = () => {
setLanguageDD((prev) => !prev);
};
return (
<button onClick={handleToggleButton} ref={LanguageDDRef}>
{languageDD ? 'Show' : 'Hide'}
</button>
);
}
Related
The component below Will set state if right or left arrow key is pressed. because each pressing key executes two set states, it will leads some performance issues. How can I get change in pressing keys without setting states or with just one set state?
import { useState, useEffect } from "react";
export function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false)
function downHandler({ key }) {
if (key === targetKey) {
setKeyPressed(true);
}
}
const upHandler = ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
}
useEffect(() => {
window.addEventListener("keydown", downHandler);
window.addEventListener("keyup", upHandler);
return () => {
window.removeEventListener("keydown", downHandler);
window.removeEventListener("keyup", upHandler);
}
})
return keyPressed
}
If you always need the current value of keyPressed, there's no getting around setting state, but you can skip the effect by specifying the dependency list and calling useRef() like this:
export function useEventListener(targetRef, type, handler) {
const handlerRef = useRef(null);
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const target = targetRef.current;
const listener = (event) => handlerRef.current(event);
target.addEventListener(type, listener);
return () => {
target.removeEventListener(type, listener);
};
}, [targetRef, type]);
}
And then you can define your function like this:
export function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);
const windowRef = useRef(window);
useEventListener(windowRef, 'keydown', ({ key }) => {
if (key === targetKey) {
setKeyPressed(true);
}
});
useEventListener(windowRef, 'keyup', ({ key }) => {
if (key === targetKey) {
setKeyPressed(false);
}
});
return keyPressed;
}
While the targetRef parameter may seem roundabout at first in the particular example above, it's very useful for instance when you want to add event listeners to JSX elements like this:
const { useEffect, useRef } = React;
function useEventListener(targetRef, type, handler) {
const handlerRef = useRef(null);
useEffect(() => {
handlerRef.current = handler;
}, [handler]);
useEffect(() => {
const target = targetRef.current;
const listener = (event) => handlerRef.current(event);
target.addEventListener(type, listener);
return () => {
target.removeEventListener(type, listener);
};
}, [targetRef, type]);
}
function App() {
const buttonRef = useRef(null);
useEventListener(buttonRef, 'click', () => {
console.log('button clicked');
});
return (
<button ref={buttonRef}>Click Me</button>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I have a react component, HomeScreen.js:
import React, { useEffect,useState } from "react";
const HomeScreen = () => {
const [scrolledY, setScrolledY] = useState(0);
const handleScroll = () => {
const scrollY = window.scrollY;
setScrolledY(scrollY);
console.log(scrolledY);
};
useEffect(() => {
window.addEventListener("scroll", handleScroll);
}, []);
return (
<div
className="homeScreenContainer"
>
</div>
</div>
);
};
Why is scrolledY not getting updated when I scroll?
Got it. I just had to add the scrolledY state as useEffect's dependency;
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [scrolledY]);
I've created a custom Hook that detects if a click was done outside of a component:
import { useEffect, useState } from 'react';
const useOutsideClick = (ref) => {
const [clickOutside, setClickOutside] = useState(false);
useEffect(() => {
const handleClick = (e) => {
ref.current?.contains(e.target)
? setClickOutside(false)
: setClickOutside(true);
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [ref]);
return clickOutside;
};
export default useOutsideClick;
I'm using the Hook on a main component. After the user clicks outside the component, the Hook needs to reset to its initial state (outsideClick = false):
const App = () => {
const [activeComponent, setActiveComponent] = useState(null);
const dropDownRef = useRef();
const outsideClick = useOutsideClick(dropDownRef);
useEffect( () => {
if(outsideClick){
setActiveComponent('WhatAreYouWorkingOn');
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// At this point, outsideClick needs to be false again
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
}
}, [outsideClick, setActiveComponent]);
return (
<div className = 'DropDown' ref = {dropDownRef}/>
);
}
export default App;
How can I reset useOutsideClick to its initial state?
I'm trying to set a state, which I fetch from an API in the form of an array.
Tried this in every way possible, doesn't work.
Any ideas on how to fix this?
instance is an axios.create that creates the instance to a localhost django server which has CORS-ALLOW-CROSS-ORIGIN True
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
fetchText();
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Do it like this,
import React, { useState } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const response = await instance.get(`/one/list/`);
setOne(response.data);
};
useEffect(() => {
fetchText();
},[ any variable you want it to fetch that again ]);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
This looks to be a good use case for a useEffect hook. Also, you need to await async functions within useEffect statement. The useEffect hook cannot be an async function in itself. Also, the original implementation would result in an infinite loop. The setState function would trigger a re-render which would then trigger the fetch function to fire off again. Consider the following:
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const fetchText = async () => {
const request= await instance.get(`/one/list/`);
request.then( r => setOne(r.data))
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
return (
<>
<div>Hello World.</div>
{one.forEach(o => (
<p>o.text</p>
))}
</>
);
};
export default OneList;
Based on you guys' advice, I came up with the below code which works fine so far.
import React, { useState, useEffect } from "react";
import { instance } from "../../stores/instance";
const OneList = () => {
const [one, setOne] = useState([]);
const [el, setEl] = useState(null);
const fetchText = async () => {
let res = await instance.get("/one/list/");
setOne(res.data);
};
useEffect(() => {
(async () => {
await fetchText();
})();
}, []);
useEffect(() => {
const handleClick = e => {
let newEl = document.createElement("input");
newEl.value = e.target.innerHTML;
newEl.id = e.target.id;
newEl.addEventListener("keypress", e => handleInput(e));
newEl.addEventListener("focusout", e => handleInput(e));
e.target.parentNode.replaceChild(newEl, e.target);
newEl.focus();
};
const handleInput = async e => {
console.log(e.type);
if (
e.target.value !== "" &&
(e.key === "Enter" || e.type === "focusout")
) {
let payload = { text: e.target.value };
try {
e.preventDefault();
let res = await instance.put(`/one/update/${e.target.id}`, payload);
let text = res.data.text;
e.target.value = text;
let newEl = document.createElement("span");
newEl.innerHTML = text;
newEl.addEventListener("click", e => handleClick(e));
newEl.id = e.target.id;
e.target.parentNode.replaceChild(newEl, e.target);
} catch (e) {
console.error(e);
}
}
};
setEl(
one.map(o => (
<p>
<span id={o.id} onClick={e => handleClick(e)}>
{o.text}
</span>
</p>
))
);
}, [one]);
return <>{el}</>;
};
export default OneList;
I have a custom hook that handles clicks outside of a component:
const useOutsideClick = (ref, callback) => {
const handleClick = e => {
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
};
useEffect(() => {
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
});
};
I have used this hook in a component like this:
const MyComponent = () => {
const container = useRef();
const [isOpen, setIsOpen] = useState(false);
useOutsideClick(container, () => {
console.log("clicked outside");
});
return (
<>
<span>another element</span>
<button onClick={() => setIsOpen(false)}>click</button>
<div ref={container}></div>
</>
);
};
The problem is when I click on the span, everything works fine. But, when I click on the button an it updates the state, it doesn't enter useOutsideClick callback function. How can I solve this problem?
The issue here is that useEffect has no dependency array, so it is being called on every render. Also, it is better to keep the definition of handleClick inside the hook as it is only going to be used in the first render; otherwise if would be defined on every render.
const useOutsideClick = (ref, callback) => {
useEffect(() => {
const handleClick = e => {
if (ref.current && !ref.current.contains(e.target)) {
callback();
}
};
document.addEventListener("click", handleClick);
return () => {
document.removeEventListener("click", handleClick);
};
}, []);
};
I found that the best solution for useOutsideClick hook is this:
function useOutsideClick(ref, handler) {
useEffect(
() => {
const listener = event => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
},
[ref, handler]
);
}