I have a simple component in which I map through an array of objects and display a li element for each object:
const [isHover, setHover] = useState(false);
return (
<ResultList>
{ props.movieList.length === 0 ? (<NoResults>No results were found...</NoResults>) : (null)}
{ props.movieList.map(movie => {
return (
<React.Fragment>
<li
key={movie.id}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}>
<p>
<span>{movie.original_title}</span>
</p>
{isHover && <AddMovieToDashboard />}
</li>
)
})}
</ResultList>
)
When I onMouseEnter on a li element I want to display another component AddMovieToDashboard. I've set this up by using the useState hook but as you might expect; this causes each li in the ul to display the AddMovieToDashboard component, and not just the one which is being hovered. I should probably use the unique key value from the li element somewhere but I can't find a good example how to do it.
How about moving the items to a separate component so they can have their own state:
function ResultListItem(props) {
const [isHover, setHover] = useState(false);
return (
<li
key={props.movie.id}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<p>
<span>{props.movie.original_title}</span>
</p>
{isHover && <AddMovieToDashboard />}
</li>
)
}
function Component(props) {
return (
<ResultList>
{ props.movieList.length === 0 ? (<NoResults>No results were found...</NoResults>) : (null) }
{ props.movieList.map(movie => {
return (
<ResultListItem movie={movie} />
)
})}
</ResultList>
)
}
Related
When I use setHover it reflects to all list data which returned from map loop. How can I use hover to reflect on itself element?
const [hover, setHover] = useState(true)
function MouseOver(event) {
setHover(true)
}
function MouseOut(event){
setHover(false)
}
{data.map((item, index) => (
//When I hover parent div I want to show the {item.arrow} div inside and not all {item.arrow} divs in the loop
<div key={index} onMouseEnter={MouseOver} onMouseLeave={MouseOut} className="flex gap-3">
<div>
{item.content}
</div>
<div hidden={hover}>
{item.arrow}
</div>
</div>
))}
If the state does not need to be controlled by the parent you can create a new component to use in the list.
Each component will then control its own hover state.
const List = ({data}) => {
return (
<div>
{
data.map((item, index) => (<Item key={index} item={item} />))
}
</div>
)
}
const Item = ({item}) => {
const [hover, setHover] = useState(true)
const mouseOver = (event) => {
setHover(true)
}
const mouseOut = (event) => {
setHover(false)
}
return (
<div onMouseEnter={mouseOver} onMouseLeave={mouseOut} className="flex gap-3">
<div>
{item.content}
</div>
<div hidden={hover}>
{item.arrow}
</div>
</div>
);
}
If the state does need to be controlled by the parent you can use a Record<number, boolean> to store the states.
const List = ({data}) => {
const [hover, setHover] = useState({})
const mouseOver = (event, index) => {
setHover(c => {
return {
...c,
[index]: true
};
})
}
const mouseOut = (event, index) => {
setHover(c => {
return {
...c,
[index]: false
};
})
}
return (
<div>
{
data.map((item, index) => (
<div
key={index}
onMouseEnter={(e) => {
mouseOver(e, index);
}}
onMouseLeave={(e) => {
mouseOut(e, index);
}}
className="flex gap-3"
>
<div>
{item.content}
</div>
<div hidden={hover[index]}>
{item.arrow}
</div>
</div>
))
}
</div>
)
}
If the state is not needed for anything other than hiding a div you could also just use CSS.
CSS will not require the component to rerender everytime you hover over it.
CSS
.hoverable-show {
display: none;
}
.hoverable-item:hover .hoverable-show {
display: block;
}
JS
const List = ({data}) => {
return (
<div>
{
data.map((item, index) => (
<div
className="flex gap-3 hoverable-item"
>
<div>
{item.content}
</div>
<div className="hoverable-show">
{item.arrow}
</div>
</div>
))
}
</div>
)
}
Preference should be CSS -> Individual State -> Parent (list) State.
This looks like a use case for the useReducer hook available right from the react library.
Here I have my div that contains Two Options ["Book" , "prices"] .
My goal is when the user select the Book option it would show the element for the book , same thing for the price one too .
here is my code :
const BooksandPrices = props => {
const option =["Book" , "Prices"]
return (
<div>
{option.map((option) => (
<div
className='bp'
onClick={(e) => {
{option == 'Book' && <PriceTable />}
{option == 'Prices' && <BookTable />}
}}
>
{option}
</div>
))}
</div>
)
}
return (
<BooksandPrices />
)
If my presumption is correct that and are the elements for each respective option, then you'll want to move them outside of the option.map and initialize a state variable to control which element is displayed
const BooksandPrices = props => {
const [selectedOption, setSelectedOption] = useState();
const option =["Book" , "Prices"]
return (
<div>
{option.map((option) => (
<div
className='bp'
onClick={() => setSelectedOption(option)}
>
{option}
</div>
))}
{selectedOption == 'Book' && <PriceTable />}
{selectedOption == 'Prices' && <BookTable />}
</div>
)
}
return (
<BooksandPrices />
)
React Hooks could solve this, you just need to add useState hook.
Also keep in mind that you should to add key attribute when you loop through an array.
const BooksandPrices = props => {
const option =["Book" , "Prices"];
const [checkedOption, setCheckedOption] = useState("Book");
return (
<div>
{option.map((option, idx) => (
<div
className='bp'
onClick={()=>setCheckedOption(option)}
key={idx}
>
{option}
</div>
))}
<div>
{checkedOption === 'Book' ? <BookTable />:<PriceTable /> }
</div>
</div>
)
}
Currently, all accordion panels are being toggled simultaneously. I've tried passing in the index to the click handler, but no luck. How do I compare the current index with the current setActive variable to open and close accordion panels individually? The data I'm working with in production do not have unique ids which is why I'm using index. Thanks for any suggestions!
demo: https://codesandbox.io/s/react-accordion-using-react-hooks-forked-fup4w?file=/components/Accordion.js:141-1323
const Accordion = (props) => {
const [setActive, setActiveState] = useState(0);
const [setHeight, setHeightState] = useState("0px");
const [setRotate, setRotateState] = useState("accordion__icon");
const content = useRef(null);
const toggleAccordion = (index) => {
setActiveState(setActive === index ? "active" : "");
setHeightState(
setActive === "active" ? "0px" : `${content.current.scrollHeight}px`
);
setRotateState(
setActive === "active" ? "accordion__icon" : "accordion__icon rotate"
);
}
return (
<div>
{data.map((item, index) => (
<div key={index} className="accordion__section">
<button
className={`accordion ${setActive}`}
onClick={() => toggleAccordion(index)}
>
<p className="accordion__title">{item.title}</p>
<Chevron className={`${setRotate}`} width={10} fill={"#777"} />
</button>
<div
ref={content}
style={{ maxHeight: `${setHeight}` }}
className="accordion__content"
>
<div>{item.content}</div>
</div>
</div>
))}
</div>
);
};
The problem in your code is that you are generating a general setActive state that is then passed to all your item in your map function. You have to change your state management in order to be able to find for each of the item if they are active or not.
I'd rework a bit your component:
const Accordion = (props) => {
const [activeIndex, setActiveIndex] = useState(0);
const content = useRef(null);
return (
<div>
{data.map((item, index) => {
const isActive = index === activeIndex
return (
<div key={index} className="accordion__section">
<button
className={`accordion ${isActive ? "active" : ""}`}
onClick={() => setActiveIndex(index)}>
<p className="accordion__title">{item.title}</p>
<Chevron className={`${isActive ? "accordion__icon" : "accordion__icon rotate" }`} width={10} fill={"#777"} />
</button>
<div
ref={content}
style={{ maxHeight: `${isActive ? "0px" : `${content.current.scrollHeight}px`}` }}
className="accordion__content">
<div>{item.content}</div>
</div>
</div>
)})}
</div>
);
};
The idea is that for each item in loop you find which on is active and then assign the classes / styles according to this. Could be even more refactored but I let you clean up now that the idea should be there :)
(I apologize for the ugly code in advance -- currently refactoring)
I'm making a Table of content where the nested content appear when I click on its parent component.
For my logic, I need to pass the value of the list key to its children but I keep receiving an undefined error or nothing at all. I tried to pass the value like this: key={node2.objectId} and keyId={node2.objectId}
I read the specifications on how to pass the key value as a prop here and here
Yet, nothing works.
Here's my code:
import React from "react";
const TocContent = (props) => {
return (
<div className="">
{props.TOC.map((header) => (
<ul
key={header.objectId}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) =>
props.handleHeaderClick(
header.level,
header.treepath,
header.containsLaw,
header.sections,
header.secNum,
header.objectId,
header.id,
e.stopPropagation(),
)
}
className="TOC TOCsection"
>
{header._id}
{props.headerIndex === header.objectId
? props.headers2.map((node2) => (
<HeaderList
key={node2.objectId}
header={node2}
props={props}
keyId={node2.objectId}
>
{console.log(props.keyId)}
//--problem is here-- {props.headerIndex2 === props.keyId
? props.headers3.map((node3) => (
<HeaderList
key={node3.objectId}
header={node3}
props={props}
>
{props.headerIndex3 === node3.objectId
? props.headers4.map((node4) => (
<HeaderList
header={node4}
key={node4.objectId}
props={props}
/>
))
: null}
</HeaderList>
))
: null}
</HeaderList>
))
: null}
</ul>
))}
</div>
);
};
const HeaderList = ({ header, props }) => {
return (
<ul
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) =>
props.handleHeaderClick(
header.level,
header.treepath,
header.containsLaw,
header.sections,
header.secNum,
header.objectId,
header.id,
e.stopPropagation(),
)
}
>
{header._id}
</ul>
);
};
export default TocContent;
I finally resorted to change the structure a bit. Instead of the code above, I opted to render the HeaderList component directly in its own component (as a child of itself). This way, I'm able to read header.objectId and make the code shorter.
Here's the new code:
import React from "react";
const TocContent = (props) => {
return (
<div className="">
{props.TOC.map((header) => (
<HeaderList key={header.objectId} header={header} props={props} />
))}
</div>
);
};
const HeaderList = ({ header, props }) => {
return (
<ul
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) =>
props.handleHeaderClick(
header.level,
header.treepath,
header.containsLaw,
header.sections,
header.secNum,
header.objectId,
header.id,
e.stopPropagation(),
)
}
>
{header._id}
{/* // if savedIndex === CurrentParent Index */}
{props.headerIndex === header.objectId &&
props.headers2.map((node2) => (
<HeaderList key={node2.objectId} header={node2} props={props} />
))}
{props.headerIndex2 === header.objectId &&
props.headers3.map((node3) => (
<HeaderList key={node3.objectId} header={node3} props={props} />
))}
{props.headerIndex3 === header.objectId &&
props.headers4.map((node4) => (
<HeaderList header={node4} key={node4.objectId} props={props} />
))}
</ul>
);
};
export default TocContent;
I understand this is maybe not the cleanest code, but an improvement nonetheless. If someone wants to propose something better, it will be much appreciated.
I have a doubt with hierarchy in react.
My Main.js has 2 articles as following:
<article className="angel__flow">
{steps.map((step, index) => {
if (procedures[step]){
return (
<Flow
key={`${index}-${step}`}
index={index}
opened={currentStepIndex === index}
procedure={procedures[step]}
/>
);
} else return "";
})}
</article>
<article className="angel__steps">
{steps.map((step, index) => {
if (procedures[step]){
return (
<Steps
key={`${index}-${step}`}
index={index}
opened={currentStepIndex === index}
procedure={procedures[step]}
commandSender={sendViewerMessage}
/>
);
} else return "";
})}
</article>
Each article has a map, and for each item, it calls a function. The first one calls the Flow function and the second one calls the Steps function.
My Flow function is the following:
function Flow({ index, opened, procedure }) {
const { t, i18n } = useTranslation();
const [show, setShow] = useState(false);
useEffect(()=>{
setShow(opened)
},[opened])
return (
<a onClick={() => setShow(!show)} className={`angel__flow__button ${show ? "active" : ""}`}>
{t(procedure.title[i18n.language])}
<span class="angel__flow__button__number">{index+1}</span>
</a>
);
}
And my Steps function is the following:
function Steps({ index, opened, procedure, commandSender }) {
const { i18n } = useTranslation();
const [selected, setSelected] = useState([]);
function clickHandler(command, index, key, procedure) {
if (!isSelected(key)) commandSender(`${command}|${index}|${procedure.id}|${key}`)
if (isSelected(key)) setSelected(selected.filter(s => s !== key))
else setSelected([...selected, (key)])
}
function isSelected(key) {
return selected.includes(key);
}
return (
<>
{ opened && (
<>
{procedure.guide &&
map(procedure.guide, (value, key) => (
<a
key={key}
className={`angel__steps__button blue ${isSelected(key) ? "active" : ""}`}
onClick={() => clickHandler('GUIDE', index, key, procedure)}
>
{value[i18n.language]}
</a>
))
}
<hr />
{procedure.error &&
map(procedure.error, (value, key) => (
<a
key={key}
className={`angel__steps__button red ${isSelected(key) ? "active" : ""}`}
onClick={() => clickHandler('ERROR', index, key, procedure)}
>
{value[i18n.language]}
</a>
))
}
{procedure.success &&
map(procedure.success, (value, key) => (
<a
key={key}
className={`angel__steps__button green ${isSelected(key) ? "active" : ""}`}
onClick={() => clickHandler('SUCCESS', index, key, procedure)}
>
{value[i18n.language]}
</a>
))
}
</>
)}
</>
);
}
What I have to do is: When I click an A tag in the Flow function, it has to open the Steps of that tag.
So, I dont know what I have to do. Maybe send back to my Main function a flag that says that I clicked that tag, so, my Steps function will have it's tag opened.
Can you guys help me with this?
pass in a click handler to Flow within Main.js:
<Flow setCurrentStepHandler={(i) => this.setState({currentStepIndex: this.state.currentStepIndex === i ? null : i})}/>
And in Flow:
<a onClick={() => {
setShow(!show)
setCurrentStepHandler(index)
}}
That way when currentStepIndex is updated, it will pass as a prop to <Steps .../>