I'm building out a simple accordion component for a product page in my Next.js/react app. I've got it mostly working, however when a user clicks open a new accordion item I need to close the active one. Here's what my component looks like:
import React, { useRef, useState } from 'react';
import css from 'classnames';
import s from './ProductAccordion.module.scss';
interface FeatureProps {
title: string;
copy: string;
}
export const ProductAccordion = ({ content }: any) => {
return (
<div className={s.productAccordion}>
{content.features.map((feature: FeatureProps) => {
const [active, setActive] = useState(false);
const activeClass = active ? 'active' : '';
const toggleAccordion = () => {
setActive(!active);
};
return (
<div
className={css(s.productAccordion__section, s[activeClass])}
key={feature.title}
>
<button className={s.sectionTitle} onClick={toggleAccordion}>
<p className={s.sectionTitle__title}>{feature.title}</p>
<span className={s.button} />
</button>
<div className={css(s.sectionContent, s[activeClass])}>
<div className={s.sectionContent__copy}>{feature.copy}</div>
</div>
</div>
);
})}
</div>
);
};
How can I get my active accordion item to close when a new one is clicked? Thanks!
I would suggest:
moving your useState hook a level higher
instead of "active" being a boolean, make it a string that you can use to identify which item should be active
hopefully a feature has a unique identifier like an id or something that you can use to identify
You could do something like:
export const ProductAccordion = ({ content }: any) => {
const [active, setActive] = useState(''); // set up your useState here, so its value is available to all children elements
return (
<div className={s.productAccordion}>
{content.features.map((feature: FeatureProps) => {
const isActive = active === feature.id // feature.id here is just a stand in for some unique identifier that each feature has
const activeClass = isActive ? 'active' : '';
const toggleAccordion = () => {
if (isActive) {
setActive(''); // if the current item is active, and you toggle it, close the accordian
} else {
setActive(feature.id) // if the current item is not active, and you toggle it, open this section
}
};
return (
<div>
{/* your code here */}
</div>
);
};
Of course, there are many approaches you could take, and I'm sure a more elegant one than this exists. But this should hopefully get you in the right direction!
Related
so I am new to React and I am trying to learn the basics. I got stuck at a point, where I try to show/hide an element on a page. The problem is that when I click Show details, it works, but Hide details must be clicked 2 times in order to do what its supposed to do.
Can you help me understand this?
import React, { useState } from 'react'
const Playground2 = () => {
let visible = false;
const [details, showDetails] = useState();
const [buttonText, changeButtonText] = useState("Show details");
const toggleVisibility = () => {
if (visible) {
showDetails("");
visible = false;
console.log(visible);
changeButtonText("Show details")
} else {
showDetails("Here are some details");
visible = true;
console.log(visible);
changeButtonText("Hide details");
}
}
return (
<>
<section>
<div className="container">
<h1>Visibility Toggle</h1>
<button onClick={toggleVisibility}>{buttonText}</button>
<p>{details}</p>
</div>
</section>
</>
)
}
export default Playground2
You should use the state in your condition. If you declare a variable like your visible one, this will be assigned on every render (every time you set the state with changeButtonText or showDetails. So every time will be set to false. You can simplify your component by doing:
import React, { useState } from 'react'
const Playground2 = () => {
const [visible, setVisible] = useState();
const toggleVisibility = () => {
setVisible(prevState => !prevState)
}
const buttonText = visible ? 'Hide details' : 'Show details'
const details = 'Here are some details'
return (
<>
<section>
<div className="container">
<h1>Visibility Toggle</h1>
<button onClick={toggleVisibility}>{buttonText}</button>
{visible && <p>{details}</p>}
</div>
</section>
</>
)
}
export default Playground2
Well it'd solve your problem if you turn visibility to state as well.
What I think happening is that when you click on button, the visibility variable is turned to false but component isn't refreshed. In order for component to get refreshed, there must be some change in state.
Maybe try that. That should do the trick.
Tip: Variables like loading, visibility, modal closed/open should be state variables.
Move let visible = false out of the component body and this will work as expected, since you are putting visible inside Component, every time the component updates false will be stored in visible.
let visible = false
const Playground 2 = () => {}
<OneProfileKeyCard
title="Qualification"
showMoreText="See all qualifications"
onShowMoreClick={() => console.log('show more')}
>
Creating, communicating, and implementing the organization's vision, mission, and overall direction Leading the development and implementation of the overall organization's strategy.
</OneProfileKeyCard>
import React from 'react'
import './OneProfileKeyCard.scss'
type Props = {
title: string
showMoreText: string
onShowMoreClick: () => void
}
export const OneProfileKeyCard: React.FC<Props> = ({
title,
showMoreText,
onShowMoreClick,
children
}) => (
<div className="one-profile-key-card">
<h3>{ title }</h3>
<div>
{ children }
</div>
<button type="button" onClick={onShowMoreClick}>
{ showMoreText }
</button>
</div>
)
could anyone help me to set up a modal? Im trying to set up a modal once onShowMoreClick is clicked that would turn the children(creating, communicating, and implementing the organization...) into a modal. So far it looks like this:
You will need to have a state-managed in the parent component of where the OneProfileKeyCard child component is called from.
Something like this
const Parent = () => {
const [modalOpen, setModalOpen] = React.useState(false)
return (
<div>
<h1>Demo</h1>
<OneProfileKeyCard
title="Qualification"
showMoreText="See all qualifications"
onShowMoreClick={() => setModalOpen(!modalOpen)}>
text ... text
</OneProfileKeyCard>
</div>
)
}
I'm not sure what else is within your components, but you'll then need a way to close the model, right now I have set the showMoreClick prop to open/close, but if that should open then set it to true and do a similar pass-through for a closing false function.
i'm building a to-do app using React Js . inside the task component i used a state to apply a certain styles for the completed task and it works fine . but , after i cliked any delete button the style of the completed task deleted . how can i prevent that ?
import Task from "../Task/Task";
import style from "./TasksList.module.css";
const TasksList = ({ tasks, deleteTaskHandler }) => {
return (
<div className={style.tasks}>
<div className="container">
{tasks.map((task, idx) => {
return (
<Task
task={task}
id={idx}
key={Math.random()}
deleteTaskHandler={deleteTaskHandler}
/>
);
})}
</div>
</div>
);
};
export default TasksList;
import { useState } from "react";
import style from "./Task.module.css";
const Task = ({ task, id, deleteTaskHandler }) => {
const [isComplete, setIsComplete] = useState(false);
const markComplete = () => {
setIsComplete(!isComplete);
};
return (
<div
className={
isComplete ? `${style.task} ${style.completed}` : `${style.task}`
}
onClick={markComplete}
>
<label>{task.desc}</label>
<button onClick={() => deleteTaskHandler(id)}> Delete </button>
</div>
);
};
export default Task;
How are you maintaining the complete status of task in higher components?
Currently you are not initializing the complete state of Task.
If the task object contains the isComplete property, then you can use as shown below
const [isComplete, setIsComplete] = useState(task.isComplete);
however, you also need to update value of completed in task. So, I would suggest to have all lower components as stateless. and maintain the state at Higher component i.e. TaskList
import style from "./Task.module.css";
const Task = ({ task, id, deleteTaskHandler, setTaskCompleteHandler }) => {
const markComplete = () => {
setTaskCompleteHandler(!task.isComplete);
};
return (
<div
className={
task.isComplete ? `${style.task} ${style.completed}` : `${style.task}`
}
onClick={markComplete}
>
<label>{task.desc}</label>
<button onClick={() => deleteTaskHandler(id)}> Delete </button>
</div>
);
};
export default Task;
Implement setTaskCompleteHandler in TaskList and pass is as prop as part of Task component reandering.
Your problem is in position of your delete button, that wrapped by div with makrComplete handler, so then you click on your delete button, markComplete fired too, so your isComplete changed and styles deleted.
To prevent this behavor, you can do a little trick with prevent default. So, try to wrap your deleteTaskHandler in another function like that:
const deleteButtonClickHandler = (e) => {
e.preventDefault();
e.stopPropagation();
deleteTaskHandler(id)
}
and your delete button makrdown should be look like this:
<button onClick={deleteButtonClickHandler}> Delete </button>
New to React and trying to build a tabular component. I know I'm reinventing the wheel but I'm trying to take this as a learning experience.
Here is how I intend to use the component:
<Tabs>
<Tabs.MenuItems>
<Tabs.MenuItem>Tab item 1</Tabs.MenuItem>
<Tabs.MenuItem>Tab item 2</Tabs.MenuItem>
<Tabs.MenuItem>Tab item 3</Tabs.MenuItem>
<Tabs.MenuItem>Tab item 4</Tabs.MenuItem>
</Tabs.MenuItems>
<Tabs.Panes>
<Tabs.Pane><div>placeholder></div></Tabs.Pane>
<Tabs.Pane><div>placeholder></div></Tabs.Pane>
<Tabs.Pane><div>placeholder></div></Tabs.Pane>
<Tabs.Pane><div>placeholder></div></Tabs.Pane>
</Tabs.Panes>
</Tabs>
My current implementation works in displaying the items properly. But the one challenge I am facing is being able to handle the onClick event for the Tabs.MenuItem. I understand that I should not be handling the onClick in the Tabs.MenuItem child component, and rather should be handled in the upmost parent Tabs component.
I tried using forwardedRef but that posed some limitations in accessing the props.children. Even if I managed to get it working syntactically, I am not even sure how the Tabs component is suppose to access that ref.
The idea here is that depending on what Tabs.MenuItem is in an active state, it will correspond to the same child Tabs.Pane component index to render that pane.
import React, { forwardRef, useState } from "react";
const Tabs = (props, { activePane }) => {
return (
props.children
);
}
const MenuItems = (props) => {
React.Children.forEach(props.children, child => {
console.log(child);
})
return (
<div className="ui secondary menu" style={props.style}>
{props.children}
</div>
)
}
const MenuItem = (props) => {
const [isActive, setActive] = useState(false);
return (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a className={isActive ? "item active" : "item"} onClick={() => setActive(!isActive)}>{props.children}</a>
)
}
// const MenuItem = forwardRef((props, ref) => (
// // eslint-disable-next-line jsx-a11y/anchor-is-valid
// <a ref={ref} className="item">{props.children}</a> // error accessing props.children
// ))
const Panes = (props) => {
return (
props.children
)
}
const Pane = (props) => {
return (
props.children
)
}
Tabs.MenuItems = MenuItems;
Tabs.MenuItem = MenuItem;
Tabs.Panes = Panes;
Tabs.Pane = Pane;
export default Tabs;
I am not looking for someone to complete the entire tabular functionality, just an example of how I can forward the children references to the topmost parent so that I can handle click events properly.
let me explain my question.
I would like to create expanding flex cards, here is the exemple on codepen : https://codepen.io/z-/pen/OBPJKK
and here is my code for each button :
basically I have a component which is called HomeButtons that generates every flex cards. Inside this component I have a smaller component called readMore. In this component I have a useState that allows me to toggle individually each button to add or retreive an active class. If the active class is present, that means that the selected button must expand and the other ones must shrink.
What I would like to do is to access the readMore state ouside of the readMore subcomponent. That way I could write a function to remove the active class from a card if the user clicks on another card like so :
function setToUnactive() {
if (readMore(true)) {
readMore(false)}
}
My question is how can I get the state of readMore outside of the readMore subcomponent ? Do I need to use useContext ? Because that seems very simple to do but I tried a lot of things and nothing works. Can I pass the state readMore as a prop of the component ReadMore ? Thank you !
import React, { useState } from 'react';
import '../style/catalogue.scss';
import collectionsItems from '../Components/collectionsItemsData';
import { Link } from "react-router-dom";
const HomeButtons = ({}) => {
function ReadMore() {
const [readMore, setReadMore] = useState(false)
function toggleSetReadMore() {
setReadMore(!readMore)
}
return (
<p className='showmore' onClick={toggleSetReadMore} className={readMore ? "collection-item active" : "collection-item"}>TOGGLE BUTTON</p>
)
}
return <div>
{collectionsItems.map((collectionItem) => {
const { id, category, img } = collectionItem;
return < article key={id} >
<img className="item-img" src={img} alt=''/>
<ReadMore />
<Link to={`/${category}`} className="item-title">{category}</Link>
</article>
})}
</div>
}
export default HomeButtons;
First of all you need extract ReadMore component from function outside!
And for your problem you can lift state up(https://reactjs.org/docs/lifting-state-up.html). And since at the same time only one item can be opened you can do something like this:
function ReadMore({ isOpened, setOpened }) {
return (
<p
onClick={setOpened}
className={isOpened ? "collection-item active" : "collection-item"}
>
TOGGLE BUTTON
</p>
);
}
const HomeButtons = () => {
const [openedItemId, setOpenedItemId] = useState(null);
return (
<div>
{collectionsItems.map((collectionItem) => {
const { id, category, img } = collectionItem;
return (
<article key={id}>
<img className="item-img" src={img} alt="" />
<ReadMore
isOpened={openedItemId === id}
setOpened={() => setOpenedItemId(id)}
/>
<Link to={`/${category}`} className="item-title">
{category}
</Link>
</article>
);
})}
</div>
);
};