A disclosure reveals or hides supporting content when you activate its trigger. Build a React FAQ list where each question controls its own answer, so several answers may stay open at once. The starter renders the complete collapsed layout; you add the state and event wiring.
Implement the default App component in App.tsx. Keep a Set<number> of open row indices and toggle one index from each question button.
aria-expanded={false} and no answer regions render.aria-expanded={true}.open.has(i).Set from the React state updater so React sees a new reference.aria-expanded on the button and preserve its aria-controls link to the answer region.You can model the visible answers as one React state value: the set of row indices that are open.
Each question is a separate switch. Opening the refund answer must not close the cancellation answer, and clicking an open question must close it. The button, chevron, and answer region must all reflect the same state.
A single number can name only one open row. A Set can contain zero, one, or several indices, so membership answers the question “is this row open?” without another flag.
const [openIndex, setOpenIndex] = useState(-1);
<button onClick={() => setOpenIndex(i)}>{faq.q}</button>
{openIndex === i && <p>{faq.a}</p>}
This stores only one index, so opening a second answer replaces the first. Clicking the same question also writes the same index instead of closing it.
import { useState } from 'react';
import './styles.css';
const faqs = [
{
q: 'Can I cancel anytime?',
a: 'Yes. Cancel from the billing page whenever you like — you keep access until the end of the current billing period.',
},
{
q: 'Do you offer refunds?',
a: "We refund any plan within 14 days of purchase, no questions asked. Contact support and we'll take care of it.",
},
{
q: 'Is my data secure?',
a: 'Every request is encrypted in transit and at rest, and we never sell your data to third parties.',
},
];
export default function App() {
const [open, setOpen] = useState<Set<number>>(() => new Set());
function toggle(i: number) {
setOpen((previous) => {
const next = new Set(previous); // React needs a new reference to re-render.
if (next.has(i)) next.delete(i);
else next.add(i);
return next;
});
}
return (
<main className="container">
<h1>FAQ Disclosure</h1>
{faqs.map((faq, i) => {
const isOpen = open.has(i);
const answerId = `faq-answer-${i}`;
const questionId = `faq-question-${i}`;
return (
<div key={faq.q} className={isOpen ? 'item open' : 'item'}>
<button
type="button"
className="q"
id={questionId}
aria-expanded={isOpen}
aria-controls={answerId}
onClick={() => toggle(i)}
>
{faq.q}
<span className="chevron" aria-hidden="true">▾</span>
</button>
{isOpen && (
<p id={answerId} className="a" role="region" aria-labelledby={questionId}>
{faq.a}
</p>
)}
</div>
);
})}
</main>
);
}
The functional updater always receives the latest set. Copying it before add or delete gives React a new reference, while isOpen drives the class, ARIA value, and conditional region from one source.
Start with an empty set. Clicking row 0 copies it and adds 0, then React renders row 0 open. Clicking row 1 produces {0, 1}, so both regions remain visible. Clicking row 0 again deletes only 0, leaving {1}.
Set reference can skip the render; copy before changing membership.open.has(i).aria-controls must match the rendered answer id for that row.This version uses an immutable record of open flags. It preserves the same questions, answer regions, identifiers, and classes.
import { useState } from 'react';
import './styles.css';
const faqs = [
{ q: 'Can I cancel anytime?', a: 'Yes. Cancel from the billing page whenever you like — you keep access until the end of the current billing period.' },
{ q: 'Do you offer refunds?', a: "We refund any plan within 14 days of purchase, no questions asked. Contact support and we'll take care of it." },
{ q: 'Is my data secure?', a: 'Every request is encrypted in transit and at rest, and we never sell your data to third parties.' },
];
export default function App() {
const [open, setOpen] = useState<Record<number, boolean>>({});
const toggle = (i: number) => setOpen((state) => ({ ...state, [i]: !state[i] }));
return (
<main className="container">
<h1>FAQ Disclosure</h1>
{faqs.map((faq, i) => {
const isOpen = Boolean(open[i]);
const questionId = `faq-question-${i}`;
const answerId = `faq-answer-${i}`;
return (
<div key={faq.q} className={isOpen ? 'item open' : 'item'}>
<button type="button" className="q" id={questionId} aria-expanded={isOpen} aria-controls={answerId} onClick={() => toggle(i)}>
{faq.q}<span className="chevron" aria-hidden="true">▾</span>
</button>
{isOpen && <p id={answerId} className="a" role="region" aria-labelledby={questionId}>{faq.a}</p>}
</div>
);
})}
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A disclosure reveals or hides supporting content when you activate its trigger. Build a React FAQ list where each question controls its own answer, so several answers may stay open at once. The starter renders the complete collapsed layout; you add the state and event wiring.
Implement the default App component in App.tsx. Keep a Set<number> of open row indices and toggle one index from each question button.
aria-expanded={false} and no answer regions render.aria-expanded={true}.open.has(i).Set from the React state updater so React sees a new reference.aria-expanded on the button and preserve its aria-controls link to the answer region.You can model the visible answers as one React state value: the set of row indices that are open.
Each question is a separate switch. Opening the refund answer must not close the cancellation answer, and clicking an open question must close it. The button, chevron, and answer region must all reflect the same state.
A single number can name only one open row. A Set can contain zero, one, or several indices, so membership answers the question “is this row open?” without another flag.
const [openIndex, setOpenIndex] = useState(-1);
<button onClick={() => setOpenIndex(i)}>{faq.q}</button>
{openIndex === i && <p>{faq.a}</p>}
This stores only one index, so opening a second answer replaces the first. Clicking the same question also writes the same index instead of closing it.
import { useState } from 'react';
import './styles.css';
const faqs = [
{
q: 'Can I cancel anytime?',
a: 'Yes. Cancel from the billing page whenever you like — you keep access until the end of the current billing period.',
},
{
q: 'Do you offer refunds?',
a: "We refund any plan within 14 days of purchase, no questions asked. Contact support and we'll take care of it.",
},
{
q: 'Is my data secure?',
a: 'Every request is encrypted in transit and at rest, and we never sell your data to third parties.',
},
];
export default function App() {
const [open, setOpen] = useState<Set<number>>(() => new Set());
function toggle(i: number) {
setOpen((previous) => {
const next = new Set(previous); // React needs a new reference to re-render.
if (next.has(i)) next.delete(i);
else next.add(i);
return next;
});
}
return (
<main className="container">
<h1>FAQ Disclosure</h1>
{faqs.map((faq, i) => {
const isOpen = open.has(i);
const answerId = `faq-answer-${i}`;
const questionId = `faq-question-${i}`;
return (
<div key={faq.q} className={isOpen ? 'item open' : 'item'}>
<button
type="button"
className="q"
id={questionId}
aria-expanded={isOpen}
aria-controls={answerId}
onClick={() => toggle(i)}
>
{faq.q}
<span className="chevron" aria-hidden="true">▾</span>
</button>
{isOpen && (
<p id={answerId} className="a" role="region" aria-labelledby={questionId}>
{faq.a}
</p>
)}
</div>
);
})}
</main>
);
}
The functional updater always receives the latest set. Copying it before add or delete gives React a new reference, while isOpen drives the class, ARIA value, and conditional region from one source.
Start with an empty set. Clicking row 0 copies it and adds 0, then React renders row 0 open. Clicking row 1 produces {0, 1}, so both regions remain visible. Clicking row 0 again deletes only 0, leaving {1}.
Set reference can skip the render; copy before changing membership.open.has(i).aria-controls must match the rendered answer id for that row.This version uses an immutable record of open flags. It preserves the same questions, answer regions, identifiers, and classes.
import { useState } from 'react';
import './styles.css';
const faqs = [
{ q: 'Can I cancel anytime?', a: 'Yes. Cancel from the billing page whenever you like — you keep access until the end of the current billing period.' },
{ q: 'Do you offer refunds?', a: "We refund any plan within 14 days of purchase, no questions asked. Contact support and we'll take care of it." },
{ q: 'Is my data secure?', a: 'Every request is encrypted in transit and at rest, and we never sell your data to third parties.' },
];
export default function App() {
const [open, setOpen] = useState<Record<number, boolean>>({});
const toggle = (i: number) => setOpen((state) => ({ ...state, [i]: !state[i] }));
return (
<main className="container">
<h1>FAQ Disclosure</h1>
{faqs.map((faq, i) => {
const isOpen = Boolean(open[i]);
const questionId = `faq-question-${i}`;
const answerId = `faq-answer-${i}`;
return (
<div key={faq.q} className={isOpen ? 'item open' : 'item'}>
<button type="button" className="q" id={questionId} aria-expanded={isOpen} aria-controls={answerId} onClick={() => toggle(i)}>
{faq.q}<span className="chevron" aria-hidden="true">▾</span>
</button>
{isOpen && <p id={answerId} className="a" role="region" aria-labelledby={questionId}>{faq.a}</p>}
</div>
);
})}
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.