Context as a contract
Every provider fills { state, actions, meta }. Any part fits any.
From vercel-composition-patterns, rule context-interface.
Plug parts into providers
Press Type-check to try every part under every provider.
Ad-hoc shapesBad
– of 12 compile
const { input, updateInput } = useChannelComposer()One interfaceGood
– of 12 compile
const { state, actions } = use(ComposerContext)interface ComposerState {
input: string
attachments: Attachment[]
isSubmitting: boolean
}
interface ComposerActions {
update: (updater: (state: ComposerState) => ComposerState) => void
submit: () => void
}
interface ComposerMeta {
inputRef: React.RefObject<TextInput>
}
interface ComposerContextValue {
state: ComposerState
actions: ComposerActions
meta: ComposerMeta
}
const ComposerContext = createContext<ComposerContextValue | null>(null)
function ComposerInput() {
const {
state,
actions: { update },
meta,
} = use(ComposerContext)
// This component works with ANY provider that implements the interface
return (
<TextInput
ref={meta.inputRef}
value={state.input}
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
/>
)
}
meta carries the refs
Use the toolbar outside the composer frame.
Ref local to InputBad
const inputRef = useRef(null) // inside Inputmeta.inputRefGood
meta.inputRef.current?.focus()function DialogToolbar() {
const { actions, meta } = use(ComposerContext)
return (
<>
<Button onPress={() => meta.inputRef.current?.focus()}>Focus</Button>
<Button onPress={() => actions.update((s) => ({ ...s, input: s.input + '@ada ' }))}>
Mention
</Button>
</>
)
}
Why it matters, pros and cons
Why it matters
One fixed shape turns providers into plug-ins. Parts code against the contract, never a hook.
Pros
- Any part works under any provider.
state,actions,metasay where each thing lives.- Refs travel without
forwardRefchains.
Cons
- A generic
update(fn)is looser than named actions. - Every provider must fill the whole contract.
metacan turn into a junk drawer.