React Patterns & Best PracticesEntry 2 of 10

Building an Accordion with Shared Context

The parent Accordion creates a context holding the open item's ID and a toggle function. Child components consume it.

Notes

  • Store openItemId (one open at a time) and a toggleItem(id) function in the parent.
  • Clicking the open item's title closes it; clicking another opens that one.
  • A custom hook (useAccordionContext) reads the context and throws if it's missing, giving a helpful error when a child is used outside <Accordion>.
  • The parent renders children inside its wrapper element.

What / Why / When

What

The state owner of the compound component.

Why

Children stay simple; the consumer doesn't manage state.

When

The first step of any compound component.

Code compare

React
// components/Accordion/Accordion.tsximport { createContext, useContext, useState, type ReactNode } from "react";​type AccordionContextValue = {  openItemId: string | null;  toggleItem: (id: string) => void;};​const AccordionContext = createContext<AccordionContextValue | null>(null);​export function useAccordionContext() {  const ctx = useContext(AccordionContext);  if (!ctx) throw new Error("Accordion-related components must be wrapped by <Accordion>.");  return ctx;}​type AccordionProps = { children: ReactNode; className?: string };​export default function Accordion({ children, className }: AccordionProps) {  const [openItemId, setOpenItemId] = useState<string | null>(null);​  function toggleItem(id: string) {    setOpenItemId((prevId) => (prevId === id ? null : id));  }​  return (    <AccordionContext value={{ openItemId, toggleItem }}>      <ul className={className}>{children}</ul>    </AccordionContext>  );}

The parent component is the shared state. No separate context object is needed.

Angular
// accordion/accordion.component.ts@Component({  selector: "app-accordion",  template: `    <ul>      <ng-content />    </ul>  `,})export class AccordionComponent {  readonly openItemId = signal<string | null>(null);​  toggleItem(id: string) {    this.openItemId.update((current) => (current === id ? null : id));  }}

Differences and gotchas

  • In React, every consumer of the context re-renders when its value changes. Keep compound contexts small and local, not app-wide.
  • Angular children can inject the parent component directly because DI follows the element tree of the template where they're declared.

Quick reference

ReactAngular
createContext + <Ctx value>Parent component with signals
useState for openItemIdsignal<string | null>
useAccordionContext() throws if missinginject(AccordionComponent) throws NullInjectorError