React Patterns & Best PracticesEntry 3 of 10
Identifying Items with a Nested Context
Each AccordionItem provides its own small context with its id, so Title and Content know which item they belong to without the consumer repeating the ID.
Notes
- Without it, every
TitleandContentwould need anidprop, duplicating the item's ID. AccordionItemtakes theidonce and provides it throughAccordionItemContext.AccordionTitlereads both contexts: the item ID, andtoggleItemfrom the accordion.AccordionContentchecksopenItemId === idto decide whether it's open.- Using a CSS class (instead of conditional rendering) for open/closed lets the content animate.
What / Why / When
- What
A second, item-level context inside the compound component.
- Why
The consumer passes each value once.
- When
Children need data that belongs to their immediate parent, not the root.
Code compare
// components/Accordion/AccordionItem.tsximport { createContext, useContext, type ReactNode } from "react";const AccordionItemContext = createContext<string | null>(null);export function useAccordionItemContext() { const id = useContext(AccordionItemContext); if (id === null) throw new Error("AccordionItem-related components must be wrapped by <Accordion.Item>."); return id;}type AccordionItemProps = { id: string; children: ReactNode; className?: string };export default function AccordionItem({ id, children, className }: AccordionItemProps) { return ( <AccordionItemContext value={id}> <li className={className}>{children}</li> </AccordionItemContext> );}Each level injects the level above it.
// accordion-item.component.ts@Component({ selector: "app-accordion-item", template: ` <li> <ng-content /> </li> `,})export class AccordionItemComponent { private accordion = inject(AccordionComponent); id = input.required<string>(); readonly isOpen = computed(() => this.accordion.openItemId() === this.id()); toggle() { this.accordion.toggleItem(this.id()); }}Differences and gotchas
- React needs a second context; Angular's item component is the item context, so
TitleandContentinject it directly. - In Angular, projected content inside
@ifis still created even when hidden. Toggling a class (as here) avoids that question; useng-templateif content must be created lazily.
Quick reference
| React | Angular |
|---|---|
AccordionItemContext with the id | AccordionItemComponent with id input |
useAccordionItemContext() | inject(AccordionItemComponent) |
openItemId === id in Content | computed() isOpen on the item |