Skip to main content
Arthur Ha

React Patterns

Compose Internals - Subcomponents Read From Context, Not Props

March 3, 2026

When you build a compound component - a family of pieces like Composer.Frame, Composer.Input, Composer.Submit - the worst path is passing state and handlers through every level via props. The better path: compose internals. Subcomponents read from a shared context. The parent provides that context; the pieces just consume it. Same UI, swappable state.

The problem: prop drilling inside the compound component

TSX
function ComposerFrame({ children, value, onChange, onSubmit }) {
  return <form onSubmit={onSubmit}>{children}</form>;
}

function ComposerInput({ value, onChange }) {
  return <TextInput value={value} onChange={onChange} />;
}

// Usage: you have to wire every piece
<ComposerFrame value={input} onChange={setInput} onSubmit={handleSubmit}>
  <ComposerInput value={input} onChange={setInput} />
  <ComposerSubmit onSubmit={handleSubmit} />
</ComposerFrame>;

Every slice of state and every callback has to be passed down. The compound component is just a prop relay. Adding a new field or action means touching every layer.

The fix: one context, subcomponents that use it

Define a single context (e.g. state, actions, meta). The provider supplies it; subcomponents read it with use() (or useContext in React 18). No props for shared state.

TSX
const ComposerContext = createContext<ComposerContextValue | null>(null);

function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
  return (
    <ComposerContext.Provider value={{ state, actions, meta }}>
      {children}
    </ComposerContext.Provider>
  );
}

function ComposerInput() {
  const { state, actions, meta } = use(ComposerContext);
  return (
    <TextInput
      ref={meta.inputRef}
      value={state.input}
      onChangeText={(text) => actions.update((s) => ({ ...s, input: text }))}
    />
  );
}

function ComposerSubmit() {
  const { actions } = use(ComposerContext);
  return <Button onPress={actions.submit}>Send</Button>;
}

// Usage: provider wires state once; pieces compose
<Composer.Provider state={state} actions={actions} meta={meta}>
  <Composer.Frame>
    <Composer.Input />
    <Composer.Footer>
      <Composer.Formatting />
      <Composer.Submit />
    </Composer.Footer>
  </Composer.Frame>
</Composer.Provider>;

The UI pieces don't know whether state comes from useState, Zustand, or a server hook. They only depend on the context interface. That's composing internals: subcomponents read from context, not from props, so you can swap implementations without changing the component tree.