Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Apply React composition patterns to build flexible, maintainable components without boolean prop sprawl
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/state-context-interface.md
1---2title: Define Generic Context Interfaces for Dependency Injection3impact: HIGH4impactDescription: enables dependency-injectable state across use-cases5tags: composition, context, state, typescript, dependency-injection6---78## Define Generic Context Interfaces for Dependency Injection910Define a **generic interface** for your component context with three parts:11`state`, `actions`, and `meta`. This interface is a contract that any provider12can implement—enabling the same UI components to work with completely different13state implementations.1415**Core principle:** Lift state, compose internals, make state16dependency-injectable.1718**Incorrect (UI coupled to specific state implementation):**1920```tsx21function ComposerInput() {22// Tightly coupled to a specific hook23const { input, setInput } = useChannelComposerState()24return <TextInput value={input} onChangeText={setInput} />25}26```2728**Correct (generic interface enables dependency injection):**2930```tsx31// Define a GENERIC interface that any provider can implement32interface ComposerState {33input: string34attachments: Attachment[]35isSubmitting: boolean36}3738interface ComposerActions {39update: (updater: (state: ComposerState) => ComposerState) => void40submit: () => void41}4243interface ComposerMeta {44inputRef: React.RefObject<TextInput>45}4647interface ComposerContextValue {48state: ComposerState49actions: ComposerActions50meta: ComposerMeta51}5253const ComposerContext = createContext<ComposerContextValue | null>(null)54```5556**UI components consume the interface, not the implementation:**5758```tsx59function ComposerInput() {60const {61state,62actions: { update },63meta,64} = use(ComposerContext)6566// This component works with ANY provider that implements the interface67return (68<TextInput69ref={meta.inputRef}70value={state.input}71onChangeText={(text) => update((s) => ({ ...s, input: text }))}72/>73)74}75```7677**Different providers implement the same interface:**7879```tsx80// Provider A: Local state for ephemeral forms81function ForwardMessageProvider({ children }: { children: React.ReactNode }) {82const [state, setState] = useState(initialState)83const inputRef = useRef(null)84const submit = useForwardMessage()8586return (87<ComposerContext88value={{89state,90actions: { update: setState, submit },91meta: { inputRef },92}}93>94{children}95</ComposerContext>96)97}9899// Provider B: Global synced state for channels100function ChannelProvider({ channelId, children }: Props) {101const { state, update, submit } = useGlobalChannel(channelId)102const inputRef = useRef(null)103104return (105<ComposerContext106value={{107state,108actions: { update, submit },109meta: { inputRef },110}}111>112{children}113</ComposerContext>114)115}116```117118**The same composed UI works with both:**119120```tsx121// Works with ForwardMessageProvider (local state)122<ForwardMessageProvider>123<Composer.Frame>124<Composer.Input />125<Composer.Submit />126</Composer.Frame>127</ForwardMessageProvider>128129// Works with ChannelProvider (global synced state)130<ChannelProvider channelId="abc">131<Composer.Frame>132<Composer.Input />133<Composer.Submit />134</Composer.Frame>135</ChannelProvider>136```137138**Custom UI outside the component can access state and actions:**139140The provider boundary is what matters—not the visual nesting. Components that141need shared state don't have to be inside the `Composer.Frame`. They just need142to be within the provider.143144```tsx145function ForwardMessageDialog() {146return (147<ForwardMessageProvider>148<Dialog>149{/* The composer UI */}150<Composer.Frame>151<Composer.Input placeholder="Add a message, if you'd like." />152<Composer.Footer>153<Composer.Formatting />154<Composer.Emojis />155</Composer.Footer>156</Composer.Frame>157158{/* Custom UI OUTSIDE the composer, but INSIDE the provider */}159<MessagePreview />160161{/* Actions at the bottom of the dialog */}162<DialogActions>163<CancelButton />164<ForwardButton />165</DialogActions>166</Dialog>167</ForwardMessageProvider>168)169}170171// This button lives OUTSIDE Composer.Frame but can still submit based on its context!172function ForwardButton() {173const {174actions: { submit },175} = use(ComposerContext)176return <Button onPress={submit}>Forward</Button>177}178179// This preview lives OUTSIDE Composer.Frame but can read composer's state!180function MessagePreview() {181const { state } = use(ComposerContext)182return <Preview message={state.input} attachments={state.attachments} />183}184```185186The `ForwardButton` and `MessagePreview` are not visually inside the composer187box, but they can still access its state and actions. This is the power of188lifting state into providers.189190The UI is reusable bits you compose together. The state is dependency-injected191by the provider. Swap the provider, keep the UI.192