I've been trying to do a sort of toggle whereby you are able to click on a question to expand the answer. I've tried adapting the code from https://codesandbox.io/s/polished-rain-xnez0?file=/src/App.js, which was from another question on here.
Instead of creating a map in the parent component and passing in them separately to a reusable 'Expandable' component to render separate functional components as shown in the example, I tried creating the map within the FAQ component:
FAQ Expandable Component:
const FAQ = ({ questions }) => {
const [expanded, setExpanded] = useState(false);
const handleClick = () => {
setExpanded((prevExpanded) => !prevExpanded);
};
const renderedQuestions = questions.map((question, index) => {
return (
<React.Fragment key={question.id}>
<FAQIndividualWrapper>
<FAQTitle
className='title'
onClick={() => handleClick()}
>
{/* <i></i> */}
{question.title}
</FAQTitle>
<FAQContent className='content' style={{ display: expanded ? "block" : "none" }}>
{question.content}
</FAQContent>
</FAQIndividualWrapper>
</React.Fragment>
)
})
return (
<>
{renderedQuestions}
</>
)
Parent Component:
const questions = [
{
id: 1,
title: 'Question 1',
content: 'Answer 1'
},
{
id: 2,
title: 'Question 2',
content: 'Answer 2'
}
]
const FAQSection = () => {
return (
<FAQPageContainer>
<FAQWrapper>
<FAQ questions={questions} />
</FAQWrapper>
</FAQPageContainer>
)
}
However, my code results in all the answers being expanded on any click of either question. Why is this happening?
Also, how should I structure and fix the code for 'ideal' programming?
Thank you!
The problem with your code is that you only have one expanded state for all questions. This means the open/collapse state is the same for all questions.
If you want to include all of your code inside one component. You need to somehow distinguish the open/collapse state of each child.
Here I'm using an array to store individual open/collapse state of each child.
// initial state is an array with the length of your questions array, default to false
const [expandedIndexes, setExpandedIndexes] = useState(
Array(info.length).fill(false)
);
const handleClick = (index) => {
setExpandedIndexes((prevExpandedIndexes) => {
const newState = [...prevExpandedIndexes];
// set state for the corresponding index
newState.splice(index, 1, !prevExpandedIndexes[index]);
return newState;
});
};
return (
<div className="details">
{info.map(({ title, details, id }, index) => (
<div key={id} className="details-wrapper">
<div>
<h3 className="title">{title}</h3>
<button onClick={() => handleClick(index)}>+</button>
</div>
<p
className="text"
// check the corresponding state to display
style={{ display: expandedIndexes[index] ? "block" : "none" }}
>
{details}
</p>
</div>
))}
</div>
);
Also, how should I structure and fix the code for 'ideal' programming?
Ideally, you want to follow the convention in your codesandbox link above. That way you don't need to deal with this kind of logic, each child will have its own state/handleClick function.
It is correct that the issue is caused by the fact that you've only a single boolean state.
Use an object to store the ids of the questions that are expanded, toggling a boolean value for each.
Example:
const FAQ = ({ questions }) => {
const [expanded, setExpanded] = useState({});
// Curried callback to enclose the id in callback scope
const handleClick = id => () => {
setExpanded(expanded => ({
...expanded,
[id]: !expanded[id],
}));
};
return questions.map((question) => (
<FAQIndividualWrapper key={question.id}>
<FAQTitle
className='title'
onClick={handleClick(question.id)}
>
{question.title}
</FAQTitle>
<FAQContent
className='content'
style={{ display: expanded[question.id] ? "block" : "none" }}
>
{question.content}
</FAQContent>
</FAQIndividualWrapper>
));
}