Hello I've run into an issue while using react-select.
In my current project I had to use react-select into a form, which is inside a popover, at which is visible by clicking
a button.
The issue that we are facing with the component is when a value is selected, the menu is removed from the DOM before we
check if the given element is inside, doing so it fails the test and closes the popover.
Below is a working poc of the issue. When we are clicking the input, opening the native select, or selecting a value
using native, it registers as inside, sadly the same behaviour doesn't apply to react-select as when a value is selected
the menu closes (it's being removed from the DOM).
Note: one requirement is to keep the accessibility implemented in react-select (like toggling menu on space key, etc.)
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import Select from 'react-select';
const PlainPopover = ({visible, children, onOutsideClick}) => {
const [ref, setRef] = useState();
useEffect(() => {
const click = e => {
const event = e;
if (!ref) return;
if (!ref.contains(event.target)) {
console.log("Outside click", ref, event.target)
onOutsideClick(event);
}
console.log("Inside click")
}
document.addEventListener("click", click)
return () => document.removeEventListener("click", click)
}, [visible, ref])
return <div ref={setRef} id={"#popover"} style={{backgroundColor: "#bababa"}}>
{children}
</div>
}
const App = () => {
const [value, setValue] = useState();
const [visible, setVisible] = useState(false);
return <>
<button onClick={() => setVisible(true)}>Open</button>
{visible && <PlainPopover visible={visible} onOutsideClick={() => setVisible(false)}>
<form style={{display: "flex", flexDirection: "column", maxWidth: 200}}>
<div>This is inside</div>
<Select options={["1", "2"].map(e => ({value: e, label: e}))}
onChange={setValue}
value={value}/>
<input/>
<select>
<option>1</option>
<option>2</option>
</select>
<div>As well as this</div>
</form>
</PlainPopover>
}
</>
}
ReactDOM.render(
<React.StrictMode>
<App/>
</React.StrictMode>,
document.getElementById('root')
);
I've tried 2 solutions:
overwriting the Menu, MenuPortal MenuList components to allways render the menu, but hiding it in css, the issue being that the visibilty isn't controlled by them.
overwriting the Control component and giving its props {..parentProps, menuIsVisible: true} but that doesn't force
the menu to be always visible
Any ideas to make it work?
Tried solutions from comments :
#Adriano Repetti : Wild guess: do not check if your ref contains the target. Do the opposite: FROM THE TARGET keep walking up the tree (parent -> parent -> ...) until you find your container or there is nothing left to check.
Using this recursive function:
const isElementParent = (element, parent) => {
console.log(element.parentNode)
while (element.parentNode) {
console.log(element);
if (element === parent) {
return true;
}
element = element.parentNode;
}
return false;
}
changed if condition from if (!ref.contains(event.target)) to !isElementParent(event.target, ref)
Related
I am curious how to architect a component leveraging MUI's Popover component when there are dynamic props getting passed to a controlled Slider component inside of the Popover component — as well as the anchor element also getting dynamically updated as the value changes getting passed-down from a higher order component.
What is happening is that when the controlled child is updated by the user, it dispatches the change higher up the chain, driving new values down, which then re-renders the component, setting the anchorEl back to null. Here's a quick video in action:
I'm sure there is something straightforward I could do to avoid this. Any help is appreciated!
Here is abbreviated code:
function Component({ dynamicProps }) {
const [anchorEl, setAnchorEl] = React.useState(null);
const { dispatch } = useContext();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleChange = (_, newValue) => {
dispatch({
body: newValue
});
};
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<div>
<Button
onClick={handleClick}
label={dynamicProps.label}
></Button>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
>
<Box sx={{ minWidth: "200px", mx: 2 }}>
<Slider
value={dynamicProps.value}
onChange={handleChange}
/>
</Box>
</Popover>
</div>
);
}
I have tried separating the Slider into another component, to avoid the re-render, and using my context's state to grab the values that I need, hover that point seems moot, since I still need to reference the anchorEl in the child, and since the trigger also is leveraging dynamic props, it will re-render and keep null-ing the anchorEl.
Ok team. Figured this one all-by-myself 🤗
Here's what you don't want to do: If you're going to use context — use it both for dispatching and grabbing state. Don't drill-down state from a parent component that will trigger a re-render. For both the button label and the controlled Slider, as long as you use the state insider the Component function through your context hook, you won't trigger a re-render, making your popper disappear from the re-render.
Do this 👇
export default function Assumption({ notDynamicProps }) {
const [anchorEl, setAnchorEl] = React.useState(null);
const { dispatch, state } = useRentalCalculator();
Not this 👇
export default function Assumption({ dynamicProps, notDynamicProps }) {
const [anchorEl, setAnchorEl] = React.useState(null);
const { dispatch } = useRentalCalculator();
I am getting this error while using useRef and useEffect in react js.
**how can i cleanup the useEffect in React js this is main topic of this all question **
Dropdown.js:9
Uncaught TypeError: Cannot read properties of null (reading 'contains')
at HTMLDocument.bodydroptoggler (Dropdown.js:9)
here is screenshot:
I am getting this error when i click on the button named as "drop toggler"
here is code of app.js
import React, { useState } from "react";
import Dropdown from "./components/Dropdown";
const options = [
{
label: "red color is selected",
value: "red",
},
{
label: "blue color is selected",
value: "blue",
},
{
label: "green color is seleted",
value: "green",
},
];
const App = () => {
const [dropactive, setDropactive] = useState(true);
return (
<div className="container ui">
<button
className="button ui"
onClick={() => setDropactive(!dropactive)}
>
drop toggler
</button>
{dropactive ? <Dropdown options={options} /> : null}
</div>
);
};
export default App;
and here is code of dropdown.js
import React, { useState, useRef, useEffect } from "react";
const Dropdown = ({ options }) => {
const [selected, setSelected] = useState(options[0]);
const [open, setOpen] = useState(false);
const ref = useRef();
useEffect(() => {
const bodydroptoggler = (event) => {
if (ref.current.contains(event.target)) {
return;
}
setOpen(false);
};
document.addEventListener("click", bodydroptoggler);
return () => {
document.removeEventListener("click", bodydroptoggler);
console.log("work");
};
}, []);
const RenderedOptions = options.map((option, index) => {
if (selected.value === option.value) {
return null;
} else {
return (
<div
className="item"
key={index}
onClick={() => {
setSelected(option);
}}
>
{option.label}
</div>
);
}
});
return (
<div ref={ref} className="ui form">
<div className="field">
<label className="text label">Select from here:</label>
<div
className={`ui selection dropdown ${
open ? "active visible" : ""
}`}
onClick={() => setOpen(!open)}
>
<i className="dropdown icon"></i>
<div className="text">{selected.label}</div>
<div className={`menu ${open ? "visible transition" : ""}`}>
{RenderedOptions}
</div>
</div>
</div>
</div>
);
};
export default Dropdown;
here is what i want to perform
i just want to hide that form by clicking on the button.
how you can run this project
just create a react app
put code of app.js to app.js of your project
dropdown.js inside the component folder
i hope this all detail will help you i you need anything more just commnet down
thanks in advance
Have you tried using optional chaining since ref.current might sometimes be undefined?
if (ref.current?.contains(event.target))
Here's a codesandbox link with the fix.
Also some additional context from React Ref docs on why sometimes the ref might be null
React will assign the current property with the DOM element when the component mounts, and assign it back to null when it unmounts.
EDIT:
This is whay useLayoutEffect is for. It runs it's contents (and cleanups) synchronously and avoids the race condition. Here's the stackblitz that proves it:
https://stackblitz.com/edit/react-5w7vog
Check out this post from Kent Dodd's as well:
One other situation you might want to use useLayoutEffect instead of useEffect is if you're updating a value (like a ref) and you want to make sure it's up-to-date before any other code runs. For example:
ORIGINAL ANSWER
It's complicated. Your code generally looks good so it took me a minute to understand why. But here's the why - the Dropdown component unmounts before the cleanup from the effect is run. So the click event still finds the handler, this time with a null reference for the ref (because the ref gets updated immediately).
Your code is correct, idomatic React - but this is an edge case that needs deeper understanding.
As the other answerer already mentioned, just add an optional check. But I thought you might like to know why.
I'm writing a Tooltip component and came across the following problem -
Function components cannot be given refs
My code looks like this
Component that includes the Tooltip:
<Tooltip title="Title" description="Description">
<Selection value={copy.value} label={copy.label} />
</Tooltip>
Tooltip component:
import React, { useRef } from 'react'
const Tooltip = props => {
const myRef = useRef()
const child = React.cloneElement(props.children, {
onMouseEnter: () => { /* placeholder */ },
onMouseLeave: () => { /* placeholder */ },
ref: myRef,
})
const TooltipOverlay = () => {
// will be a div wrapped in react Portal
return null
}
return (
<>
{child}
<TooltipOverlay />
</>
)
}
export default Tooltip
My goal is to not have a visible wrapper in the DOM like div/span or similar.
I need the ref to be able to relatively position TooltipOverlay in the Tooltip component.
All solutions are welcome as long as long as goals above are fulfilled.
I saw this done in rsuite library, not sure how.
Had the same issue and couldn't find a way to do what you wanted.
As a workaround and I guess the usual way is to let the user of the Tooltip pass the ref down and then you can intercept it with forwardRef.
const Tooltip = forwardRef((props, myRef) => {
// now you can clone without a ref and have access to the ref.
})
The user of the tooltip needs to pass a ref however:
const childRef = useRef()
<Tooltip ref={childRef}>
<div ref={childRef} />
</Tooltip>
I am working with a form in react, and what I would like is that when I click a button, I add a new component which is just an input to the screen. It all mostly works, as planned. The issue is with the following: the layout is that I have one main component, which then displays a child component. That child component is called from a map of a useState. (More after code snippet)
This is the code of the main component:
import React, { useState } from "react";
import SingleProfile from "./individual_profile";
const ProfileInformation = (props) => {
console.log("proflie render");
const [ProfilesBoolean, setProfilesBoolean] = useState(false);
const [profiles, setProfiles] = useState(props.Data['profiles'])
const FieldAdd = (event)=>{
event.preventDefault();
const copy = profiles;
copy.push({Network:'',url:''})
return(copy)
}
function CreateInput(){
return profiles.map((data, index) =><SingleProfile index={index} data={data} />)
}
const accordion = (event) => {
const NextElement = event.target.nextElementSibling;
if (!event.target.className.includes("display")) {
NextElement.style.maxHeight = NextElement.scrollHeight + "px";
} else {
NextElement.style.maxHeight = 0;
}
};
return (
<div className="AccordionItem">
<div
className={
ProfilesBoolean ? "AccordionHeader-display" : "AccordionHeader"
}
onClick={(e) => setProfilesBoolean(!ProfilesBoolean)}
id="ProfileForm"
>
Profiles
</div>
<div className="AccordionContent">
<div className="AccordionBody">
{
profiles.map((data, index) => (
<SingleProfile index={index} data={data} />
))
}
<button id="ProfileAdd" onClick={(e) => {setProfiles(FieldAdd(e))}}>
Add a profile
</button>
</div>
</div>
</div>
);
};
export default ProfileInformation;
When I click the button and onClick fires FieldAdd() the useState updates, with a new empty object as expected. However, it does not appear inside my <div className="AccordionBody"> as I would expect it to.
The following code is used to display components, by opening and closing the child div. When it is open is when you see the child components and the add button. If I click the div, to close and then click again to re-open it, the new child component appears.
<div
className={ProfilesBoolean ? "AccordionHeader-display" : "AccordionHeader"}
onClick={(e) => setProfilesBoolean(!ProfilesBoolean)}
id="ProfileForm"
>
Profiles
</div>;
Is it possible to have the child component appear without having to close and re-open the div?
Your clickHandler FieldAdd is incorrect. You are mutating the state directly which will not cause re-render.
use setProfiles to update the state in the clickHandler. Like this
const FieldAdd = (event)=>{
setProfiles(prev => [...prev, {Network:'',url:''}])
}
Trigger the onClick like this
<button id="ProfileAdd" onClick={(e) => {FieldAdd(e)}}>
Add a profile
</button>
...
I am trying to create an accordion component using React, but the animation is not working.
The basic idea is, I believe, pretty standard, I am giving each item body a max-height of 0 which is affected by adding a show class to an element. I am able to select and show the item I want, but the animation to slide in/out is not working.
With the Chrome dev tools open, when I click on one of the items I can see that the whole "accordion" element is flashing, which leads me to believe that the whole element is being re-rendered. But I am unsure why this would be the case.
Here is the relevant Accordion component:
import React, { useState } from "react";
const Accordion = ({ items }) => {
const [selectedItem, setSelectedItem] = useState(0);
const AccordionItem = ({ item, index }) => {
const isOpen = index === selectedItem;
return (
<div className="accordion-item">
<div
onClick={() => {
setSelectedItem(index);
}}
className="accordion-header"
>
<div>{item.heading}</div>
</div>
<div className={`accordion-body ${isOpen ? "show" : ""}`}>
<div className="accordion-content">{item.body}</div>
</div>
</div>
);
};
return (
<div className="accordion">
{items.map((item, i) => {
return <AccordionItem key={i} item={item} index={i} />;
})}
</div>
);
};
export default Accordion;
And here is a codepen illustrating the problem:
https://codesandbox.io/s/heuristic-heyrovsky-xgcbe
of course, its going to re-render. When ever you call setSelectedIem, state changes and hence react re-renders on state change to exhibit that change.
Now if you place this
const [selectedItem, setSelectedItem] = useState(0);
inside Accordion Item, it would just re-render accordion item, but would mess up your functionality.