Only the provider knows how

UI reads the context. Swapping how state is stored touches one file.

From vercel-composition-patterns, rule decouple-implementation.

Swap the state engine

Swap the provider, then type, send or remount.

UI calls the hooksBad
const state = useGlobalChannelState(channelId)
Provider owns itGood
<ChannelProvider><ChannelComposer /></ChannelProvider>
// Provider handles all state management details
function ChannelProvider({ channelId, children }: {
  channelId: string
  children: React.ReactNode
}) {
  const { state, update, submit } = useGlobalChannel(channelId)
  const inputRef = useRef(null)

  return (
    <Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
      {children}
    </Composer.Provider>
  )
}

// UI component only knows about the context interface
function ChannelComposer() {
  return (
    <Composer.Frame>
      <Composer.Header />
      <Composer.Input />
      <Composer.Footer>
        <Composer.Submit />
      </Composer.Footer>
    </Composer.Frame>
  )
}

Test the UI with a fake

Press Run test. Count what each test has to stub.

Mock the hooksBad
0 module mocksidle
vi.mock('@/state/channel')
Pass a fake providerGood
0 module mocksidle
<Composer.Provider state={fake} …>

Why it matters, pros and cons

Why it matters

When UI reads only the context interface, how state is stored is a detail one file owns.

Pros

  • Change storage without touching UI files.
  • Same UI in forms, dialogs and synced views.
  • Test UI with a plain fake provider.

Cons

  • One more layer to open when debugging.
  • Too generic an interface hides real differences, like offline.
  • A tiny single-use widget doesn't need it.