Deft
Ask AI

Search

Search skills, authors and topics

vercel-labs / agent-skills

vercel-composition-patterns

React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.

Install

npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-composition-patterns
title
Decouple State Management from UI
impact
MEDIUM
impactDescription
enables swapping state implementations without changing UI
tags
composition, state, architecture

Decouple State Management from UI

The provider component should be the only place that knows how state is managed. UI components consume the context interface—they don't know if state comes from useState, Zustand, or a server sync.

Incorrect (UI coupled to state implementation):

tsx
function ChannelComposer({ channelId }: { channelId: string }) {  // UI component knows about global state implementation  const state = useGlobalChannelState(channelId)  const { submit, updateInput } = useChannelSync(channelId)
  return (    <Composer.Frame>      <Composer.Input        value={state.input}        onChange={(text) => sync.updateInput(text)}      />      <Composer.Submit onPress={() => sync.submit()} />    </Composer.Frame>  )}

Correct (state management isolated in provider):

tsx
// Provider handles all state management detailsfunction 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 interfacefunction ChannelComposer() {  return (    <Composer.Frame>      <Composer.Header />      <Composer.Input />      <Composer.Footer>        <Composer.Submit />      </Composer.Footer>    </Composer.Frame>  )}
// Usagefunction Channel({ channelId }: { channelId: string }) {  return (    <ChannelProvider channelId={channelId}>      <ChannelComposer />    </ChannelProvider>  )}

Different providers, same UI:

tsx
// Local state for ephemeral formsfunction ForwardMessageProvider({ children }) {  const [state, setState] = useState(initialState)  const forwardMessage = useForwardMessage()
  return (    <Composer.Provider      state={state}      actions={{ update: setState, submit: forwardMessage }}    >      {children}    </Composer.Provider>  )}
// Global synced state for channelsfunction ChannelProvider({ channelId, children }) {  const { state, update, submit } = useGlobalChannel(channelId)
  return (    <Composer.Provider state={state} actions={{ update, submit }}>      {children}    </Composer.Provider>  )}

The same Composer.Input component works with both providers because it only depends on the context interface, not the implementation.