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.