feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
29
skills/vercel-composition-patterns/rules/_sections.md
Normal file
29
skills/vercel-composition-patterns/rules/_sections.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Sections
|
||||
|
||||
This file defines all sections, their ordering, impact levels, and descriptions.
|
||||
The section ID (in parentheses) is the filename prefix used to group rules.
|
||||
|
||||
---
|
||||
|
||||
## 1. Component Architecture (architecture)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** Fundamental patterns for structuring components to avoid prop
|
||||
proliferation and enable flexible composition.
|
||||
|
||||
## 2. State Management (state)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Patterns for lifting state and managing shared context across
|
||||
composed components.
|
||||
|
||||
## 3. Implementation Patterns (patterns)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** Specific techniques for implementing compound components and
|
||||
context providers.
|
||||
|
||||
## 4. React 19 APIs (react19)
|
||||
|
||||
**Impact:** MEDIUM
|
||||
**Description:** React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.
|
||||
24
skills/vercel-composition-patterns/rules/_template.md
Normal file
24
skills/vercel-composition-patterns/rules/_template.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Rule Title Here
|
||||
impact: MEDIUM
|
||||
impactDescription: brief description of impact
|
||||
tags: composition, components
|
||||
---
|
||||
|
||||
## Rule Title Here
|
||||
|
||||
Brief explanation of the rule and why it matters.
|
||||
|
||||
**Incorrect:**
|
||||
|
||||
```tsx
|
||||
// Bad code example
|
||||
```
|
||||
|
||||
**Correct:**
|
||||
|
||||
```tsx
|
||||
// Good code example
|
||||
```
|
||||
|
||||
Reference: [Link](https://example.com)
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Avoid Boolean Prop Proliferation
|
||||
impact: CRITICAL
|
||||
impactDescription: prevents unmaintainable component variants
|
||||
tags: composition, props, architecture
|
||||
---
|
||||
|
||||
## Avoid Boolean Prop Proliferation
|
||||
|
||||
Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize
|
||||
component behavior. Each boolean doubles possible states and creates
|
||||
unmaintainable conditional logic. Use composition instead.
|
||||
|
||||
**Incorrect (boolean props create exponential complexity):**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
onSubmit,
|
||||
isThread,
|
||||
channelId,
|
||||
isDMThread,
|
||||
dmId,
|
||||
isEditing,
|
||||
isForwarding,
|
||||
}: Props) {
|
||||
return (
|
||||
<form>
|
||||
<Header />
|
||||
<Input />
|
||||
{isDMThread ? (
|
||||
<AlsoSendToDMField id={dmId} />
|
||||
) : isThread ? (
|
||||
<AlsoSendToChannelField id={channelId} />
|
||||
) : null}
|
||||
{isEditing ? (
|
||||
<EditActions />
|
||||
) : isForwarding ? (
|
||||
<ForwardActions />
|
||||
) : (
|
||||
<DefaultActions />
|
||||
)}
|
||||
<Footer onSubmit={onSubmit} />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (composition eliminates conditionals):**
|
||||
|
||||
```tsx
|
||||
// Channel composer
|
||||
function ChannelComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Attachments />
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
}
|
||||
|
||||
// Thread composer - adds "also send to channel" field
|
||||
function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<AlsoSendToChannelField id={channelId} />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
}
|
||||
|
||||
// Edit composer - different footer actions
|
||||
function EditComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.CancelEdit />
|
||||
<Composer.SaveEdit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Each variant is explicit about what it renders. We can share internals without
|
||||
sharing a single monolithic parent.
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Use Compound Components
|
||||
impact: HIGH
|
||||
impactDescription: enables flexible composition without prop drilling
|
||||
tags: composition, compound-components, architecture
|
||||
---
|
||||
|
||||
## Use Compound Components
|
||||
|
||||
Structure complex components as compound components with a shared context. Each
|
||||
subcomponent accesses shared state via context, not props. Consumers compose the
|
||||
pieces they need.
|
||||
|
||||
**Incorrect (monolithic component with render props):**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
renderActions,
|
||||
showAttachments,
|
||||
showFormatting,
|
||||
showEmojis,
|
||||
}: Props) {
|
||||
return (
|
||||
<form>
|
||||
{renderHeader?.()}
|
||||
<Input />
|
||||
{showAttachments && <Attachments />}
|
||||
{renderFooter ? (
|
||||
renderFooter()
|
||||
) : (
|
||||
<Footer>
|
||||
{showFormatting && <Formatting />}
|
||||
{showEmojis && <Emojis />}
|
||||
{renderActions?.()}
|
||||
</Footer>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (compound components with shared context):**
|
||||
|
||||
```tsx
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
||||
|
||||
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
||||
return (
|
||||
<ComposerContext value={{ state, actions, meta }}>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
}
|
||||
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>
|
||||
}
|
||||
|
||||
function ComposerInput() {
|
||||
const {
|
||||
state,
|
||||
actions: { update },
|
||||
meta: { inputRef },
|
||||
} = use(ComposerContext)
|
||||
return (
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={state.input}
|
||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function ComposerSubmit() {
|
||||
const {
|
||||
actions: { submit },
|
||||
} = use(ComposerContext)
|
||||
return <Button onPress={submit}>Send</Button>
|
||||
}
|
||||
|
||||
// Export as compound component
|
||||
const Composer = {
|
||||
Provider: ComposerProvider,
|
||||
Frame: ComposerFrame,
|
||||
Input: ComposerInput,
|
||||
Submit: ComposerSubmit,
|
||||
Header: ComposerHeader,
|
||||
Footer: ComposerFooter,
|
||||
Attachments: ComposerAttachments,
|
||||
Formatting: ComposerFormatting,
|
||||
Emojis: ComposerEmojis,
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
<Composer.Provider state={state} actions={actions} meta={meta}>
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</Composer.Provider>
|
||||
```
|
||||
|
||||
Consumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Prefer Composing Children Over Render Props
|
||||
impact: MEDIUM
|
||||
impactDescription: cleaner composition, better readability
|
||||
tags: composition, children, render-props
|
||||
---
|
||||
|
||||
## Prefer Children Over Render Props
|
||||
|
||||
Use `children` for composition instead of `renderX` props. Children are more
|
||||
readable, compose naturally, and don't require understanding callback
|
||||
signatures.
|
||||
|
||||
**Incorrect (render props):**
|
||||
|
||||
```tsx
|
||||
function Composer({
|
||||
renderHeader,
|
||||
renderFooter,
|
||||
renderActions,
|
||||
}: {
|
||||
renderHeader?: () => React.ReactNode
|
||||
renderFooter?: () => React.ReactNode
|
||||
renderActions?: () => React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<form>
|
||||
{renderHeader?.()}
|
||||
<Input />
|
||||
{renderFooter ? renderFooter() : <DefaultFooter />}
|
||||
{renderActions?.()}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// Usage is awkward and inflexible
|
||||
return (
|
||||
<Composer
|
||||
renderHeader={() => <CustomHeader />}
|
||||
renderFooter={() => (
|
||||
<>
|
||||
<Formatting />
|
||||
<Emojis />
|
||||
</>
|
||||
)}
|
||||
renderActions={() => <SubmitButton />}
|
||||
/>
|
||||
)
|
||||
```
|
||||
|
||||
**Correct (compound components with children):**
|
||||
|
||||
```tsx
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>
|
||||
}
|
||||
|
||||
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
||||
return <footer className='flex'>{children}</footer>
|
||||
}
|
||||
|
||||
// Usage is flexible
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<CustomHeader />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<SubmitButton />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
```
|
||||
|
||||
**When render props are appropriate:**
|
||||
|
||||
```tsx
|
||||
// Render props work well when you need to pass data back
|
||||
<List
|
||||
data={items}
|
||||
renderItem={({ item, index }) => <Item item={item} index={index} />}
|
||||
/>
|
||||
```
|
||||
|
||||
Use render props when the parent needs to provide data or state to the child.
|
||||
Use children when composing static structure.
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Create Explicit Component Variants
|
||||
impact: MEDIUM
|
||||
impactDescription: self-documenting code, no hidden conditionals
|
||||
tags: composition, variants, architecture
|
||||
---
|
||||
|
||||
## Create Explicit Component Variants
|
||||
|
||||
Instead of one component with many boolean props, create explicit variant
|
||||
components. Each variant composes the pieces it needs. The code documents
|
||||
itself.
|
||||
|
||||
**Incorrect (one component, many modes):**
|
||||
|
||||
```tsx
|
||||
// What does this component actually render?
|
||||
<Composer
|
||||
isThread
|
||||
isEditing={false}
|
||||
channelId='abc'
|
||||
showAttachments
|
||||
showFormatting={false}
|
||||
/>
|
||||
```
|
||||
|
||||
**Correct (explicit variants):**
|
||||
|
||||
```tsx
|
||||
// Immediately clear what this renders
|
||||
<ThreadComposer channelId="abc" />
|
||||
|
||||
// Or
|
||||
<EditMessageComposer messageId="xyz" />
|
||||
|
||||
// Or
|
||||
<ForwardMessageComposer messageId="123" />
|
||||
```
|
||||
|
||||
Each implementation is unique, explicit and self-contained. Yet they can each
|
||||
use shared parts.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```tsx
|
||||
function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<ThreadProvider channelId={channelId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<AlsoSendToChannelField channelId={channelId} />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ThreadProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||
return (
|
||||
<EditMessageProvider messageId={messageId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.CancelEdit />
|
||||
<Composer.SaveEdit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</EditMessageProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||
return (
|
||||
<ForwardMessageProvider messageId={messageId}>
|
||||
<Composer.Frame>
|
||||
<Composer.Input placeholder="Add a message, if you'd like." />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
<Composer.Mentions />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ForwardMessageProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Each variant is explicit about:
|
||||
|
||||
- What provider/state it uses
|
||||
- What UI elements it includes
|
||||
- What actions are available
|
||||
|
||||
No boolean prop combinations to reason about. No impossible states.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: React 19 API Changes
|
||||
impact: MEDIUM
|
||||
impactDescription: cleaner component definitions and context usage
|
||||
tags: react19, refs, context, hooks
|
||||
---
|
||||
|
||||
## React 19 API Changes
|
||||
|
||||
> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.
|
||||
|
||||
In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.
|
||||
|
||||
**Incorrect (forwardRef in React 19):**
|
||||
|
||||
```tsx
|
||||
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
||||
return <TextInput ref={ref} {...props} />
|
||||
})
|
||||
```
|
||||
|
||||
**Correct (ref as a regular prop):**
|
||||
|
||||
```tsx
|
||||
function ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
|
||||
return <TextInput ref={ref} {...props} />
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (useContext in React 19):**
|
||||
|
||||
```tsx
|
||||
const value = useContext(MyContext)
|
||||
```
|
||||
|
||||
**Correct (use instead of useContext):**
|
||||
|
||||
```tsx
|
||||
const value = use(MyContext)
|
||||
```
|
||||
|
||||
`use()` can also be called conditionally, unlike `useContext()`.
|
||||
@@ -0,0 +1,191 @@
|
||||
---
|
||||
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 <TextInput value={input} onChangeText={setInput} />
|
||||
}
|
||||
```
|
||||
|
||||
**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<TextInput>
|
||||
}
|
||||
|
||||
interface ComposerContextValue {
|
||||
state: ComposerState
|
||||
actions: ComposerActions
|
||||
meta: ComposerMeta
|
||||
}
|
||||
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(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 (
|
||||
<TextInput
|
||||
ref={meta.inputRef}
|
||||
value={state.input}
|
||||
onChangeText={(text) => 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 (
|
||||
<ComposerContext
|
||||
value={{
|
||||
state,
|
||||
actions: { update: setState, submit },
|
||||
meta: { inputRef },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
}
|
||||
|
||||
// Provider B: Global synced state for channels
|
||||
function ChannelProvider({ channelId, children }: Props) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId)
|
||||
const inputRef = useRef(null)
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
value={{
|
||||
state,
|
||||
actions: { update, submit },
|
||||
meta: { inputRef },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**The same composed UI works with both:**
|
||||
|
||||
```tsx
|
||||
// Works with ForwardMessageProvider (local state)
|
||||
<ForwardMessageProvider>
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Submit />
|
||||
</Composer.Frame>
|
||||
</ForwardMessageProvider>
|
||||
|
||||
// Works with ChannelProvider (global synced state)
|
||||
<ChannelProvider channelId="abc">
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Submit />
|
||||
</Composer.Frame>
|
||||
</ChannelProvider>
|
||||
```
|
||||
|
||||
**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 (
|
||||
<ForwardMessageProvider>
|
||||
<Dialog>
|
||||
{/* The composer UI */}
|
||||
<Composer.Frame>
|
||||
<Composer.Input placeholder="Add a message, if you'd like." />
|
||||
<Composer.Footer>
|
||||
<Composer.Formatting />
|
||||
<Composer.Emojis />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
|
||||
{/* Custom UI OUTSIDE the composer, but INSIDE the provider */}
|
||||
<MessagePreview />
|
||||
|
||||
{/* Actions at the bottom of the dialog */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton />
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ForwardMessageProvider>
|
||||
)
|
||||
}
|
||||
|
||||
// This button lives OUTSIDE Composer.Frame but can still submit based on its context!
|
||||
function ForwardButton() {
|
||||
const {
|
||||
actions: { submit },
|
||||
} = use(ComposerContext)
|
||||
return <Button onPress={submit}>Forward</Button>
|
||||
}
|
||||
|
||||
// This preview lives OUTSIDE Composer.Frame but can read composer's state!
|
||||
function MessagePreview() {
|
||||
const { state } = use(ComposerContext)
|
||||
return <Preview message={state.input} attachments={state.attachments} />
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
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 details
|
||||
function 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 interface
|
||||
function ChannelComposer() {
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Header />
|
||||
<Composer.Input />
|
||||
<Composer.Footer>
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
}
|
||||
|
||||
// Usage
|
||||
function Channel({ channelId }: { channelId: string }) {
|
||||
return (
|
||||
<ChannelProvider channelId={channelId}>
|
||||
<ChannelComposer />
|
||||
</ChannelProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Different providers, same UI:**
|
||||
|
||||
```tsx
|
||||
// Local state for ephemeral forms
|
||||
function 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 channels
|
||||
function 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.
|
||||
125
skills/vercel-composition-patterns/rules/state-lift-state.md
Normal file
125
skills/vercel-composition-patterns/rules/state-lift-state.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: Lift State into Provider Components
|
||||
impact: HIGH
|
||||
impactDescription: enables state sharing outside component boundaries
|
||||
tags: composition, state, context, providers
|
||||
---
|
||||
|
||||
## Lift State into Provider Components
|
||||
|
||||
Move state management into dedicated provider components. This allows sibling
|
||||
components outside the main UI to access and modify state without prop drilling
|
||||
or awkward refs.
|
||||
|
||||
**Incorrect (state trapped inside component):**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageComposer() {
|
||||
const [state, setState] = useState(initialState)
|
||||
const forwardMessage = useForwardMessage()
|
||||
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input />
|
||||
<Composer.Footer />
|
||||
</Composer.Frame>
|
||||
)
|
||||
}
|
||||
|
||||
// Problem: How does this button access composer state?
|
||||
function ForwardMessageDialog() {
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer />
|
||||
<MessagePreview /> {/* Needs composer state */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton /> {/* Needs to call submit */}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (useEffect to sync state up):**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const [input, setInput] = useState('')
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer onInputChange={setInput} />
|
||||
<MessagePreview input={input} />
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ onInputChange }) {
|
||||
const [state, setState] = useState(initialState)
|
||||
useEffect(() => {
|
||||
onInputChange(state.input) // Sync on every change 😬
|
||||
}, [state.input])
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (reading state from ref on submit):**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const stateRef = useRef(null)
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer stateRef={stateRef} />
|
||||
<ForwardButton onPress={() => submit(stateRef.current)} />
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (state lifted to provider):**
|
||||
|
||||
```tsx
|
||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState(initialState)
|
||||
const forwardMessage = useForwardMessage()
|
||||
const inputRef = useRef(null)
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
state={state}
|
||||
actions={{ update: setState, submit: forwardMessage }}
|
||||
meta={{ inputRef }}
|
||||
>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function ForwardMessageDialog() {
|
||||
return (
|
||||
<ForwardMessageProvider>
|
||||
<Dialog>
|
||||
<ForwardMessageComposer />
|
||||
<MessagePreview /> {/* Custom components can access state and actions */}
|
||||
<DialogActions>
|
||||
<CancelButton />
|
||||
<ForwardButton /> {/* Custom components can access state and actions */}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ForwardMessageProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function ForwardButton() {
|
||||
const { actions } = use(Composer.Context)
|
||||
return <Button onPress={actions.submit}>Forward</Button>
|
||||
}
|
||||
```
|
||||
|
||||
The ForwardButton lives outside the Composer.Frame but still has access to the
|
||||
submit action because it's within the provider. Even though it's a one-off
|
||||
component, it can still access the composer's state and actions from outside the
|
||||
UI itself.
|
||||
|
||||
**Key insight:** Components that need shared state don't have to be visually
|
||||
nested inside each other—they just need to be within the same provider.
|
||||
Reference in New Issue
Block a user