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 atoggleItem(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
childreninside 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
// 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.
// 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
| React | Angular |
|---|---|
createContext + <Ctx value> | Parent component with signals |
useState for openItemId | signal<string | null> |
useAccordionContext() throws if missing | inject(AccordionComponent) throws NullInjectorError |