--- title: Define Generic Context Interfaces for Dependency Injection impact: HIGH impactDescription: enables dependency-injectable state across use-cases tags: composition, context, state, typescript, dependency-injection --- ## Define Generic Context Interfaces for Dependency Injection Define a **generic interface** for your component context with three parts: `state`, `actions`, and `meta`. This interface is a contract that any provider can implement—enabling the same UI components to work with completely different state implementations. **Core principle:** Lift state, compose internals, make state dependency-injectable. **Incorrect (UI coupled to specific state implementation):** ```tsx function ComposerInput() { // Tightly coupled to a specific hook const { input, setInput } = useChannelComposerState() return } ``` **Correct (generic interface enables dependency injection):** ```tsx // Define a GENERIC interface that any provider can implement interface ComposerState { input: string attachments: Attachment[] isSubmitting: boolean } interface ComposerActions { update: (updater: (state: ComposerState) => ComposerState) => void submit: () => void } interface ComposerMeta { inputRef: React.RefObject } interface ComposerContextValue { state: ComposerState actions: ComposerActions meta: ComposerMeta } const ComposerContext = createContext(null) ``` **UI components consume the interface, not the implementation:** ```tsx function ComposerInput() { const { state, actions: { update }, meta, } = use(ComposerContext) // This component works with ANY provider that implements the interface return ( update((s) => ({ ...s, input: text }))} /> ) } ``` **Different providers implement the same interface:** ```tsx // Provider A: Local state for ephemeral forms function ForwardMessageProvider({ children }: { children: React.ReactNode }) { const [state, setState] = useState(initialState) const inputRef = useRef(null) const submit = useForwardMessage() return ( {children} ) } // Provider B: Global synced state for channels function ChannelProvider({ channelId, children }: Props) { const { state, update, submit } = useGlobalChannel(channelId) const inputRef = useRef(null) return ( {children} ) } ``` **The same composed UI works with both:** ```tsx // Works with ForwardMessageProvider (local state) // Works with ChannelProvider (global synced state) ``` **Custom UI outside the component can access state and actions:** The provider boundary is what matters—not the visual nesting. Components that need shared state don't have to be inside the `Composer.Frame`. They just need to be within the provider. ```tsx function ForwardMessageDialog() { return ( {/* The composer UI */} {/* Custom UI OUTSIDE the composer, but INSIDE the provider */} {/* Actions at the bottom of the dialog */} ) } // This button lives OUTSIDE Composer.Frame but can still submit based on its context! function ForwardButton() { const { actions: { submit }, } = use(ComposerContext) return } // This preview lives OUTSIDE Composer.Frame but can read composer's state! function MessagePreview() { const { state } = use(ComposerContext) return } ``` The `ForwardButton` and `MessagePreview` are not visually inside the composer box, but they can still access its state and actions. This is the power of lifting state into providers. The UI is reusable bits you compose together. The state is dependency-injected by the provider. Swap the provider, keep the UI.