I have this component, and I am using useRef and useEffect to handle a click outside a popup so I can close it.
I have added the two dependencies the useEffect needs but I get this error:
The 'handleClickOutside' function makes the dependencies of useEffect Hook (at line 117) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'handleClickOutside' definition into its own useCallback()
As you can see here, I am adding both dependencies, but it still throws this error/warning:
useEffect(() => {
if (isOverlayOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen, handleClickOutside]);
Any ideas how to fix this?
Here i the codesandbox: https://codesandbox.io/s/laughing-newton-1gcme?fontsize=14&hidenavigation=1&theme=dark The problem is in src/components/typeahead line 100
And here the component code:
function ResultsOverlay({
isOpen,
items,
selectItem,
highlightedOption,
setIsOverlayOpen,
isOverlayOpen
}) {
const node = useRef();
const handleClickOutside = e => {
if (node.current.contains(e.target)) {
return;
}
setIsOverlayOpen(false);
};
useEffect(() => {
if (isOverlayOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen]);
function matchedOptionsClass(index) {
if (highlightedOption === index) {
return "ph4 list f3 sesame-red list-item pointer";
}
return "ph4 list sesame-blue list-item pointer";
}
if (isOpen) {
return (
<div className="absolute" ref={node}>
<ul className="w5 mt0 pa0 h5 overflow-scroll shadow-5 dib">
{items &&
items.map((item, index) => (
<li
onClick={() => selectItem(item)}
className={matchedOptionsClass(index)}
>
{item}
</li>
))}
</ul>
</div>
);
} else {
return null;
}
}
Two problems:
First, do what the linst is telling you and move your function definition inside your effect
useEffect(() => {
const handleClickOutside = e => {
if (node.current.contains(e.target)) {
return;
}
setIsOverlayOpen(false);
};
if (isOverlayOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen]);
Second, setIsOverlayOpen is a callback provided via props, so it doesn't have a stable signature and triggers the effect on each render.
Assuming that setIsOverlayOpen is a setter from useState and doesn't need to change it's signature you can workaround this by wrapping your handler in an aditional dependency check layer by using useCallback
const stableHandler = useCallback(setIsOverlayOpen, [])
useEffect(() => {
const handleClickOutside = e => {
if (node.current.contains(e.target)) {
return;
}
stableHandler(false);
};
if (isOverlayOpen) {
document.addEventListener("mousedown", handleClickOutside);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOverlayOpen, stableHandler]);
Related
I am passing functions to my child component. And I am using React.memo to restrict compoenent from re-rendering. But My component rerenders when parent re-renders. I tried to check why this is happening by using useEffect on all the props and I get to this point that my functions are causing compoenent to re-renders.
// my functions
const scrollToView = (index) => {
if (scrollRef && scrollRef.current && scrollRef.current[index]) {
scrollRef.current[index].scrollIntoView({ behavior: 'smooth' });
}
};
const scrollToReportView = (reportIndex) => {
if (scrollToReportRef && scrollToReportRef.current &&
scrollToReportRef.current[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: 'smooth' });
}
}
.......
function LeftNav({
scrollToView, //function
scrollToReportView, //function
reports, //object
}) {
useEffect(() => {
console.log('scrollToView')
}, [scrollToView])
useEffect(() => {
console.log('scrollToReportView')
}, [scrollToReportView])
useEffect(() => {
console.log('reports')
}, [reports])
return (
<div>{'My Child Component'}</div>
);
}
export default memo(LeftNav);
And this is how my left nav is being called
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex)=> scrollToReportView(repIndex)}
reports={reports}
/>
With
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex)=> scrollToReportView(repIndex)}
reports={reports}
/>
you're creating new anonymous functions every time you render the LeftNav component, so memoization does absolutely nothing.
Just
<LeftNav
scrollToView={scrollToView}
scrollToReportView={scrollToReportView}
reports={reports}
/>
instead (assuming those functions are stable by identity (e.g. are declared outside the component or are properly React.useCallbacked or React.useMemoed).
In other words, if your component is currently
function Component() {
// ...
const scrollToView = (index) => {
if (scrollRef && scrollRef.current && scrollRef.current[index]) {
scrollRef.current[index].scrollIntoView({ behavior: "smooth" });
}
};
const scrollToReportView = (reportIndex) => {
if (scrollToReportRef && scrollToReportRef.current && scrollToReportRef.current[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: "smooth",
});
}
};
return (
<LeftNav
scrollToView={(index) => scrollToView(index)}
scrollToReportView={(repIndex) => scrollToReportView(repIndex)}
reports={reports}
/>,
);
}
it needs to be something like
function Component() {
// ...
const scrollToView = React.useCallback((index) => {
if (scrollRef?.current?.[index]) {
scrollRef.current[index].scrollIntoView({ behavior: "smooth" });
}
}, []);
const scrollToReportView = React.useCallback((reportIndex) => {
if (scrollToReportRef?.current?.[reportIndex]) {
scrollToReportRef.current[reportIndex].scrollIntoView({
behavior: "smooth",
});
}
}, []);
return (<LeftNav scrollToView={scrollToView} scrollToReportView={scrollToReportView} reports={reports} />);
}
so the scrollToView and scrollToReportView functions have stable identities.
I have a list of items and want to include an icon that opens a modal for a user to choose 'edit' or 'delete' the item.
And I put this code inside the ActionModal so that only clicked modal would open by comparing the ids.
The problem is, clicking outside the element work only one time and after that, nothing happens when the ellipsis button clicked. I think it's probably because the state inside ActionModal, 'modalOpen' remains false, but I'm stuck here and don't know how to handle it.
if (!isOpen.show || isOpen.id !== id || !modalOpen) return null;
const List = () => {
const [modal, setModal] = useState({ id: null, show: false });
const onDialogClick = (e) => {
setModal((prevState) => {
return { id: e.target.id, show: !prevState.show };
});
};
const journals = journals.map((journal) => (
<StyledList key={journal.id}>
<Option>
<FontAwesomeIcon
icon={faEllipsisV}
id={journal.id}
onClick={onDialogClick}
/>
<ActionModal
actions={['edit', 'delete']}
id={journal.id}
isOpen={modal}
></ActionModal>
</Option>
const ActionModal = ({ id, actions, isOpen }) => {
const content = actions.map((action) => <li key={action}>{action}</li>);
const ref = useRef();
const [modalOpen, setModalOpen] = useState(true);
useOnClickOutside(ref, () => setModalOpen(!modalOpen));
if (!isOpen.show || isOpen.id !== id || !modalOpen) return null;
return (
<StyledDiv>
<ul ref={ref}>{content}</ul>
</StyledDiv>
);
};
function useOnClickOutside(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]);
}
So fistly you do not need to include ref and handler as dependencies in useEffect hook, because evvent listeners are set on initial load, there is no need to set it on every value change.
I think I don't fully understand your situation. So you need to close the modal after it is opened by pressing outside it? or you want to be able to press that 3 dots icon when it's opened?
P.S.
I little bit condesed your code. Try this and let me know what is happening. :)
const ActionModal = ({ id, actions, isOpen, setOpen }) => {
const ref = useRef();
const content = actions.map((action) => <li key={action}>{action}</li>);
useEffect(() => {
const listener = (event) => {
if (ref.current || !ref.current.contains(event.target)) {
setOpen({...open, show: false});
}
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, []);
return (
<StyledDiv>
<div ref={ref}>
<ul>{content}</ul>
</div>
</StyledDiv>
);
};
I try to change myState inside scroll handle function like
const [myState, setMyState] = useState(false);
const refA = useRef(null)
const handle = (e) => {
if (!myState) {
console.log('Set', myState)
setMyState(true);
}else {
console.log('no set')
}
}
useEffect(() => {
if (props.run) {
const ref= refA.current
ref.addEventListener("scroll", handle, { passive: true });
}
}, [props.run]);
useEffect(() => {
const ref= refA.current
return () => {
ref.removeEventListener("scroll", handle, { passive: true });
}
}, [])
return (
<div ref={refA}>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
<p>long</p><br/>
</div>
);
But when i scroll my div, log is always show Set? myState can't change data? how to fix that thanks.
Just add myState in the useEffect. And add removeEventListener in return of this useEffect, don't need create other useEffect
useEffect(() => {
if (props.run) {
const ref = refA.current;
ref.addEventListener("scroll", handle, { passive: true });
return () => {
ref.removeEventListener("scroll", handle, { passive: true });
};
}
}, [props.run, myState]);
Add myState in the dependencies will call useEffect when this state change. And the variable in the handle will be updated with new state.
You should return removeEventListener after call addEventListener to make sure when useEffect call again, the old event will be removed.
I have a code similar to this one:
function Component1(...) {
...
function checkIfCtrlKey(event) {
return event.ctrlKey;
}
return (
{ checkIfCtrlKey() && (<Component2 .../>) }
);
}
The sense behind this is that the Component2 is just rendered if the Ctrl-key is being pressed. When running this code, I get following error message: TypeError: Cannot read property 'ctrlKey' of undefined
What is my mistake? Is there a solution or other possibility to implement my need?
You need to put an event listener on the window object and within that hander you can set some state to switch between a true and false
Something like this.
function Component1() {
const [ctrlActive, setCtrlActive] = useState(false)
useEffect(() => {
window.addEventListener('keydown', (e => {
if("Do a check here to see if it's CTRL key") {
setCtrlActive(!ctrlActive)
}
}), [])
})
return ctrlActive ? <Component2 /> : null
}
You can use Vanilla JS for that like this:
import React, { useEffect, useState } from "react";
export default function App() {
const [ctrlPressed, setCtrlPressed] = useState(false);
const handleKey = (e) => setCtrlPressed(e.ctrlKey);
useEffect(() => {
window.addEventListener("keydown", handleKey);
window.addEventListener("keyup", handleKey);
return function cleanup() {
window.removeEventListener("keydown", handleKey);
window.removeEventListener("keyup", handleKey);
};
}, []);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
{ctrlPressed ? <h2>You're pressing CTRL Key</h2> : null}
</div>
);
}
Working example over here
Your making mistake here,
{ checkIfCtrlKey() && (<Component2 .../>) }
refer
function checkIfCtrlKey(event) {
return event.ctrlKey;
}
How u suppose that checkIfCtrlKey will be passed with event arg when your calling like this checkIfCtrlKey() ??
You might wanted to attach it to window,
function Component1() {
const [ctrlKeyPressed, setCKP] = useState(false)
const handleKey = ev => {
setCKP(ev.ctrlKey)
}
useEffect(() => {
window.addEventListener('keydown', handleKey);
window.addEventListener('keyup', handleKey);
return () => {
window.removeEventListener('keydown', handleKey);
window.removeEventListener('keyup', handleKey);
}
}, [])
return (
<>
...
{ctrlKeyPressed && <Component2 />}
...
</>
)
}
Shows Component2 as long as ctrlKey is pressed
I have the following pseudo code
const handleUploadValidateResult = useCallback(e => {
if (everything good) {
do something
} else {
do something else
}
}, []);
useEffect(() => {
const eventName = `${context}_${type}_${index}`;
window.addEventListener(eventName, e => {
handleUploadValidateResult(e);
});
return () => {
window.removeEventListener(eventName, e => {
handleUploadValidateResult(e);
});
};
}, [type, index]);
What is the execution order for the return statement
return () => {
...
}
When type or index got changed, is return statement executed
before useEffect?
or after useEffect?
Your useEffect it called after type or index changes. return function is called before the component is unmounted.