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 Title and Content would need an id prop, duplicating the item's ID.
  • AccordionItem takes the id once and provides it through AccordionItemContext.
  • AccordionTitle reads both contexts: the item ID, and toggleItem from the accordion.
  • AccordionContent checks openItemId === id to 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

React
// 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.

Angular
// 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 Title and Content inject it directly.
  • In Angular, projected content inside @if is still created even when hidden. Toggling a class (as here) avoids that question; use ng-template if content must be created lazily.

Quick reference

ReactAngular
AccordionItemContext with the idAccordionItemComponent with id input
useAccordionItemContext()inject(AccordionItemComponent)
openItemId === id in Contentcomputed() isOpen on the item