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.
Related
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.
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)
I have created an Accordion component which has data(object) and expanded(boolean) as props.
expanded props is used to set the expanded/collapsed state of this component passed as a prop.
const DeltaAccordion = ({ index, data, expanded = true }) => {
Accordion component also has an internal state
const [isExpanded, setIsExpanded] = useState(expanded);
which is used for expanding/collapsing the accordion.
Below is my complete component
Accordion.jsx
import React, { useState } from "react";
// styles
import styles from "./index.module.scss";
const Accordion = ({ index, data, expanded = true }) => {
// state
const [isExpanded, setIsExpanded] = useState(expanded);
console.log(data.name, `prop-val==${expanded}`, `inner-state==${isExpanded}`);
return (
<div
className={`${styles.container} caption ${isExpanded && styles.expanded}`}
>
<div className={styles.header} onClick={() => setIsExpanded(!isExpanded)}>
<div>{data.name}</div>
<div>Click</div>
</div>
<div className={styles.content}>
{data.newValue && (
<div className={styles.newValue}>
<span>{data.newValue}</span>
</div>
)}
{data.oldValue && (
<div className={styles.oldValue}>
<span>{data.oldValue}</span>
</div>
)}
</div>
</div>
);
};
export default Accordion;
The parent component has a list of data and it loops through the list to create an accordion for each data item.
App.js file
{dataList.map((data, index) => (
<Accordion data={data} expanded={!collpaseAll} index={1} />
))}
Here goes problem
There is also a button in my App.js file which is for either expanding/collapsing all the accordions together.
But when I pass its value as prop expanded to the accordion component then this value is not getting applied to the internal isExpanded state of the accordion component.
Here is live running code at codesandbox: https://codesandbox.io/s/goofy-nobel-qfxm1?file=/src/App.js:635-745
Inside the Accordion
const [isExpanded, setIsExpanded] = useState(expanded);
This line will take first time(on first render) value. To assign it next time(rerender) value you need to add a effect
useEffect(() => {
setIsExpanded(expanded);
}, [expanded]);
And in your case, you can use the props expanded directly inside Accordion, you dont need to take it in local state.
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 using redux with react but i save redux for main app state, and using react hooks for non-shared state between components.
For Example, i have a navbar menu items, every item color should turn blue once get clicked, so my mind goes to useState to change the item that get clicked and add a class .active to it.
The problem i face is that i do not get the latest state, but i get the previous state and i understand that setting state in react is asynchronous and batched.
Demo Code:
Component
const [targets, getTarget] = useState([]);
const addTarget = (e) => {
getTarget([e.target]);
}
const handleOnItemClick = (e, category) => {
addTarget(e);
console.log(targets) // return [] on first click
}
myFunctionToAddAcTiveClass () {
//i need to use state here but i could not
// because it does not return the latest state
}
return(
<ul className="categories-list-items d-flex justify-content-between mb-4">
{items.map((item, index) => {
return (
<li
key={index}
onClick={(e) => handleOnItemClick(e, category)}
>
{item}
</li>
);
})}
</ul>
)
Is there a way to get the right item on every click to add the .active class??