| 1 | --- |
| 2 | title: Decouple State Management from UI |
| 3 | impact: MEDIUM |
| 4 | impactDescription: enables swapping state implementations without changing UI |
| 5 | tags: composition, state, architecture |
| 6 | --- |
| 7 | |
| 8 | ## Decouple State Management from UI |
| 9 | |
| 10 | The provider component should be the only place that knows how state is managed. |
| 11 | UI components consume the context interface—they don't know if state comes from |
| 12 | useState, Zustand, or a server sync. |
| 13 | |
| 14 | **Incorrect (UI coupled to state implementation):** |
| 15 | |
| 16 | ```tsx |
| 17 | function ChannelComposer({ channelId |
| 18 | // UI component knows about global state implementation |
| 19 | const state = |
| 20 | const { submit, |
| 21 | |
| 22 | return ( |
| 23 | <Composer.Frame> |
| 24 | <Composer.Input value= |
| 25 | <Composer.Submit onPress= |
| 26 | </Composer.Frame> |
| 27 | ); |
| 28 | } |
| 29 | ``` |
| 30 | |
| 31 | **Correct (state management isolated in provider):** |
| 32 | |
| 33 | ```tsx |
| 34 | // Provider handles all state management details |
| 35 | function ChannelProvider({ |
| 36 | channelId, |
| 37 | children, |
| 38 | }: { |
| 39 | channelId: string |
| 40 | children: React |
| 41 | }) { |
| 42 | const { state, |
| 43 | const inputRef = |
| 44 | |
| 45 | return ( |
| 46 | <Composer.Provider state= |
| 47 | {children} |
| 48 | </Composer.Provider> |
| 49 | ); |
| 50 | } |
| 51 | |
| 52 | // UI component only knows about the context interface |
| 53 | function ChannelComposer() { |
| 54 | return ( |
| 55 | <Composer.Frame> |
| 56 | <Composer.Header /> |
| 57 | <Composer.Input /> |
| 58 | <Composer.Footer> |
| 59 | <Composer.Submit /> |
| 60 | </Composer.Footer> |
| 61 | </Composer.Frame> |
| 62 | ); |
| 63 | } |
| 64 | |
| 65 | // Usage |
| 66 | function Channel({ channelId |
| 67 | return ( |
| 68 | <ChannelProvider channelId= |
| 69 | <ChannelComposer /> |
| 70 | </ChannelProvider> |
| 71 | ); |
| 72 | } |
| 73 | ``` |
| 74 | |
| 75 | **Different providers, same UI:** |
| 76 | |
| 77 | ```tsx |
| 78 | // Local state for ephemeral forms |
| 79 | function ForwardMessageProvider({ children |
| 80 | const [state, |
| 81 | const forwardMessage = |
| 82 | |
| 83 | return ( |
| 84 | <Composer.Provider state= |
| 85 | {children} |
| 86 | </Composer.Provider> |
| 87 | ); |
| 88 | } |
| 89 | |
| 90 | // Global synced state for channels |
| 91 | function ChannelProvider({ channelId |
| 92 | const { state, |
| 93 | |
| 94 | return ( |
| 95 | <Composer.Provider state= |
| 96 | {children} |
| 97 | </Composer.Provider> |
| 98 | ); |
| 99 | } |
| 100 | ``` |
| 101 | |
| 102 | The same `Composer.Input` component works with both providers because it only |
| 103 | depends on the context interface, not the implementation. |