format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
This commit is contained in:
@@ -73,16 +73,10 @@ function Composer({
|
||||
) : isThread ? (
|
||||
<AlsoSendToChannelField id={channelId} />
|
||||
) : null}
|
||||
{isEditing ? (
|
||||
<EditActions />
|
||||
) : isForwarding ? (
|
||||
<ForwardActions />
|
||||
) : (
|
||||
<DefaultActions />
|
||||
)}
|
||||
{isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
|
||||
<Footer onSubmit={onSubmit} />
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -102,7 +96,7 @@ function ChannelComposer() {
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Thread composer - adds "also send to channel" field
|
||||
@@ -118,7 +112,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Edit composer - different footer actions
|
||||
@@ -133,7 +127,7 @@ function EditComposer() {
|
||||
<Composer.SaveEdit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -177,25 +171,21 @@ function Composer({
|
||||
</Footer>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct: compound components with shared context**
|
||||
|
||||
```tsx
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||
|
||||
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
||||
return (
|
||||
<ComposerContext value={{ state, actions, meta }}>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;
|
||||
}
|
||||
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>
|
||||
return <form>{children}</form>;
|
||||
}
|
||||
|
||||
function ComposerInput() {
|
||||
@@ -203,21 +193,21 @@ function ComposerInput() {
|
||||
state,
|
||||
actions: { update },
|
||||
meta: { inputRef },
|
||||
} = use(ComposerContext)
|
||||
} = 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>
|
||||
} = use(ComposerContext);
|
||||
return <Button onPress={submit}>Send</Button>;
|
||||
}
|
||||
|
||||
// Export as compound component
|
||||
@@ -231,7 +221,7 @@ const Composer = {
|
||||
Attachments: ComposerAttachments,
|
||||
Formatting: ComposerFormatting,
|
||||
Emojis: ComposerEmojis,
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
@@ -275,18 +265,15 @@ useState, Zustand, or a server sync.
|
||||
```tsx
|
||||
function ChannelComposer({ channelId }: { channelId: string }) {
|
||||
// UI component knows about global state implementation
|
||||
const state = useGlobalChannelState(channelId)
|
||||
const { submit, updateInput } = useChannelSync(channelId)
|
||||
const state = useGlobalChannelState(channelId);
|
||||
const { submit, updateInput } = useChannelSync(channelId);
|
||||
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input
|
||||
value={state.input}
|
||||
onChange={(text) => sync.updateInput(text)}
|
||||
/>
|
||||
<Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
|
||||
<Composer.Submit onPress={() => sync.submit()} />
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -298,21 +285,17 @@ function ChannelProvider({
|
||||
channelId,
|
||||
children,
|
||||
}: {
|
||||
channelId: string
|
||||
children: React.ReactNode
|
||||
channelId: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId)
|
||||
const inputRef = useRef(null)
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
state={state}
|
||||
actions={{ update, submit }}
|
||||
meta={{ inputRef }}
|
||||
>
|
||||
<Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// UI component only knows about the context interface
|
||||
@@ -325,7 +308,7 @@ function ChannelComposer() {
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
@@ -334,7 +317,7 @@ function Channel({ channelId }: { channelId: string }) {
|
||||
<ChannelProvider channelId={channelId}>
|
||||
<ChannelComposer />
|
||||
</ChannelProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -343,28 +326,25 @@ function Channel({ channelId }: { channelId: string }) {
|
||||
```tsx
|
||||
// Local state for ephemeral forms
|
||||
function ForwardMessageProvider({ children }) {
|
||||
const [state, setState] = useState(initialState)
|
||||
const forwardMessage = useForwardMessage()
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
state={state}
|
||||
actions={{ update: setState, submit: forwardMessage }}
|
||||
>
|
||||
<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)
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
|
||||
return (
|
||||
<Composer.Provider state={state} actions={{ update, submit }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -393,8 +373,8 @@ dependency-injectable.
|
||||
```tsx
|
||||
function ComposerInput() {
|
||||
// Tightly coupled to a specific hook
|
||||
const { input, setInput } = useChannelComposerState()
|
||||
return <TextInput value={input} onChangeText={setInput} />
|
||||
const { input, setInput } = useChannelComposerState();
|
||||
return <TextInput value={input} onChangeText={setInput} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -403,27 +383,27 @@ function ComposerInput() {
|
||||
```tsx
|
||||
// Define a GENERIC interface that any provider can implement
|
||||
interface ComposerState {
|
||||
input: string
|
||||
attachments: Attachment[]
|
||||
isSubmitting: boolean
|
||||
input: string;
|
||||
attachments: Attachment[];
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
interface ComposerActions {
|
||||
update: (updater: (state: ComposerState) => ComposerState) => void
|
||||
submit: () => void
|
||||
update: (updater: (state: ComposerState) => ComposerState) => void;
|
||||
submit: () => void;
|
||||
}
|
||||
|
||||
interface ComposerMeta {
|
||||
inputRef: React.RefObject<TextInput>
|
||||
inputRef: React.RefObject<TextInput>;
|
||||
}
|
||||
|
||||
interface ComposerContextValue {
|
||||
state: ComposerState
|
||||
actions: ComposerActions
|
||||
meta: ComposerMeta
|
||||
state: ComposerState;
|
||||
actions: ComposerActions;
|
||||
meta: ComposerMeta;
|
||||
}
|
||||
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||
```
|
||||
|
||||
**UI components consume the interface, not the implementation:**
|
||||
@@ -434,7 +414,7 @@ function ComposerInput() {
|
||||
state,
|
||||
actions: { update },
|
||||
meta,
|
||||
} = use(ComposerContext)
|
||||
} = use(ComposerContext);
|
||||
|
||||
// This component works with ANY provider that implements the interface
|
||||
return (
|
||||
@@ -443,7 +423,7 @@ function ComposerInput() {
|
||||
value={state.input}
|
||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -452,9 +432,9 @@ function ComposerInput() {
|
||||
```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()
|
||||
const [state, setState] = useState(initialState);
|
||||
const inputRef = useRef(null);
|
||||
const submit = useForwardMessage();
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
@@ -466,13 +446,13 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Provider B: Global synced state for channels
|
||||
function ChannelProvider({ channelId, children }: Props) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId)
|
||||
const inputRef = useRef(null)
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
@@ -484,7 +464,7 @@ function ChannelProvider({ channelId, children }: Props) {
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -534,21 +514,21 @@ function ForwardMessageDialog() {
|
||||
</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>
|
||||
} = 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} />
|
||||
const { state } = use(ComposerContext);
|
||||
return <Preview message={state.input} attachments={state.attachments} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -582,15 +562,15 @@ or awkward refs.
|
||||
|
||||
```tsx
|
||||
function ForwardMessageComposer() {
|
||||
const [state, setState] = useState(initialState)
|
||||
const forwardMessage = useForwardMessage()
|
||||
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?
|
||||
@@ -604,7 +584,7 @@ function ForwardMessageDialog() {
|
||||
<ForwardButton /> {/* Needs to call submit */}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -612,20 +592,20 @@ function ForwardMessageDialog() {
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const [input, setInput] = useState('')
|
||||
const [input, setInput] = useState('');
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer onInputChange={setInput} />
|
||||
<MessagePreview input={input} />
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ onInputChange }) {
|
||||
const [state, setState] = useState(initialState)
|
||||
const [state, setState] = useState(initialState);
|
||||
useEffect(() => {
|
||||
onInputChange(state.input) // Sync on every change 😬
|
||||
}, [state.input])
|
||||
onInputChange(state.input); // Sync on every change 😬
|
||||
}, [state.input]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -633,13 +613,13 @@ function ForwardMessageComposer({ onInputChange }) {
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const stateRef = useRef(null)
|
||||
const stateRef = useRef(null);
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer stateRef={stateRef} />
|
||||
<ForwardButton onPress={() => submit(stateRef.current)} />
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -647,9 +627,9 @@ function ForwardMessageDialog() {
|
||||
|
||||
```tsx
|
||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState(initialState)
|
||||
const forwardMessage = useForwardMessage()
|
||||
const inputRef = useRef(null)
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
@@ -659,7 +639,7 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageDialog() {
|
||||
@@ -674,12 +654,12 @@ function ForwardMessageDialog() {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ForwardMessageProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardButton() {
|
||||
const { actions } = use(Composer.Context)
|
||||
return <Button onPress={actions.submit}>Forward</Button>
|
||||
const { actions } = use(Composer.Context);
|
||||
return <Button onPress={actions.submit}>Forward</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -718,13 +698,7 @@ itself.
|
||||
|
||||
```tsx
|
||||
// What does this component actually render?
|
||||
<Composer
|
||||
isThread
|
||||
isEditing={false}
|
||||
channelId='abc'
|
||||
showAttachments
|
||||
showFormatting={false}
|
||||
/>
|
||||
<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
|
||||
```
|
||||
|
||||
**Correct: explicit variants**
|
||||
@@ -760,7 +734,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ThreadProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||
@@ -776,7 +750,7 @@ function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</EditMessageProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||
@@ -791,7 +765,7 @@ function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ForwardMessageProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -823,9 +797,9 @@ function Composer({
|
||||
renderFooter,
|
||||
renderActions,
|
||||
}: {
|
||||
renderHeader?: () => React.ReactNode
|
||||
renderFooter?: () => React.ReactNode
|
||||
renderActions?: () => React.ReactNode
|
||||
renderHeader?: () => React.ReactNode;
|
||||
renderFooter?: () => React.ReactNode;
|
||||
renderActions?: () => React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<form>
|
||||
@@ -834,7 +808,7 @@ function Composer({
|
||||
{renderFooter ? renderFooter() : <DefaultFooter />}
|
||||
{renderActions?.()}
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Usage is awkward and inflexible
|
||||
@@ -849,18 +823,18 @@ return (
|
||||
)}
|
||||
renderActions={() => <SubmitButton />}
|
||||
/>
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
**Correct: compound components with children**
|
||||
|
||||
```tsx
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>
|
||||
return <form>{children}</form>;
|
||||
}
|
||||
|
||||
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
||||
return <footer className='flex'>{children}</footer>
|
||||
return <footer className="flex">{children}</footer>;
|
||||
}
|
||||
|
||||
// Usage is flexible
|
||||
@@ -874,17 +848,14 @@ return (
|
||||
<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} />}
|
||||
/>
|
||||
<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.
|
||||
@@ -911,28 +882,28 @@ In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `
|
||||
|
||||
```tsx
|
||||
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
||||
return <TextInput ref={ref} {...props} />
|
||||
})
|
||||
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} />
|
||||
return <TextInput ref={ref} {...props} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect: useContext in React 19**
|
||||
|
||||
```tsx
|
||||
const value = useContext(MyContext)
|
||||
const value = useContext(MyContext);
|
||||
```
|
||||
|
||||
**Correct: use instead of useContext**
|
||||
|
||||
```tsx
|
||||
const value = use(MyContext)
|
||||
const value = use(MyContext);
|
||||
```
|
||||
|
||||
`use()` can also be called conditionally, unlike `useContext()`.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: vercel-composition-patterns
|
||||
description:
|
||||
React composition patterns that scale. Use when refactoring components with
|
||||
description: 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
|
||||
|
||||
+5
-11
@@ -32,16 +32,10 @@ function Composer({
|
||||
) : isThread ? (
|
||||
<AlsoSendToChannelField id={channelId} />
|
||||
) : null}
|
||||
{isEditing ? (
|
||||
<EditActions />
|
||||
) : isForwarding ? (
|
||||
<ForwardActions />
|
||||
) : (
|
||||
<DefaultActions />
|
||||
)}
|
||||
{isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}
|
||||
<Footer onSubmit={onSubmit} />
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -61,7 +55,7 @@ function ChannelComposer() {
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Thread composer - adds "also send to channel" field
|
||||
@@ -77,7 +71,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Edit composer - different footer actions
|
||||
@@ -92,7 +86,7 @@ function EditComposer() {
|
||||
<Composer.SaveEdit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+9
-13
@@ -37,25 +37,21 @@ function Composer({
|
||||
</Footer>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Correct (compound components with shared context):**
|
||||
|
||||
```tsx
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||
|
||||
function ComposerProvider({ children, state, actions, meta }: ProviderProps) {
|
||||
return (
|
||||
<ComposerContext value={{ state, actions, meta }}>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;
|
||||
}
|
||||
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>
|
||||
return <form>{children}</form>;
|
||||
}
|
||||
|
||||
function ComposerInput() {
|
||||
@@ -63,21 +59,21 @@ function ComposerInput() {
|
||||
state,
|
||||
actions: { update },
|
||||
meta: { inputRef },
|
||||
} = use(ComposerContext)
|
||||
} = 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>
|
||||
} = use(ComposerContext);
|
||||
return <Button onPress={submit}>Send</Button>;
|
||||
}
|
||||
|
||||
// Export as compound component
|
||||
@@ -91,7 +87,7 @@ const Composer = {
|
||||
Attachments: ComposerAttachments,
|
||||
Formatting: ComposerFormatting,
|
||||
Emojis: ComposerEmojis,
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
+9
-12
@@ -19,9 +19,9 @@ function Composer({
|
||||
renderFooter,
|
||||
renderActions,
|
||||
}: {
|
||||
renderHeader?: () => React.ReactNode
|
||||
renderFooter?: () => React.ReactNode
|
||||
renderActions?: () => React.ReactNode
|
||||
renderHeader?: () => React.ReactNode;
|
||||
renderFooter?: () => React.ReactNode;
|
||||
renderActions?: () => React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<form>
|
||||
@@ -30,7 +30,7 @@ function Composer({
|
||||
{renderFooter ? renderFooter() : <DefaultFooter />}
|
||||
{renderActions?.()}
|
||||
</form>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Usage is awkward and inflexible
|
||||
@@ -45,18 +45,18 @@ return (
|
||||
)}
|
||||
renderActions={() => <SubmitButton />}
|
||||
/>
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
**Correct (compound components with children):**
|
||||
|
||||
```tsx
|
||||
function ComposerFrame({ children }: { children: React.ReactNode }) {
|
||||
return <form>{children}</form>
|
||||
return <form>{children}</form>;
|
||||
}
|
||||
|
||||
function ComposerFooter({ children }: { children: React.ReactNode }) {
|
||||
return <footer className='flex'>{children}</footer>
|
||||
return <footer className="flex">{children}</footer>;
|
||||
}
|
||||
|
||||
// Usage is flexible
|
||||
@@ -70,17 +70,14 @@ return (
|
||||
<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} />}
|
||||
/>
|
||||
<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.
|
||||
|
||||
+4
-10
@@ -15,13 +15,7 @@ itself.
|
||||
|
||||
```tsx
|
||||
// What does this component actually render?
|
||||
<Composer
|
||||
isThread
|
||||
isEditing={false}
|
||||
channelId='abc'
|
||||
showAttachments
|
||||
showFormatting={false}
|
||||
/>
|
||||
<Composer isThread isEditing={false} channelId="abc" showAttachments showFormatting={false} />
|
||||
```
|
||||
|
||||
**Correct (explicit variants):**
|
||||
@@ -56,7 +50,7 @@ function ThreadComposer({ channelId }: { channelId: string }) {
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ThreadProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||
@@ -72,7 +66,7 @@ function EditMessageComposer({ messageId }: { messageId: string }) {
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</EditMessageProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||
@@ -87,7 +81,7 @@ function ForwardMessageComposer({ messageId }: { messageId: string }) {
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
</ForwardMessageProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+5
-5
@@ -15,28 +15,28 @@ In React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `
|
||||
|
||||
```tsx
|
||||
const ComposerInput = forwardRef<TextInput, Props>((props, ref) => {
|
||||
return <TextInput ref={ref} {...props} />
|
||||
})
|
||||
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} />
|
||||
return <TextInput ref={ref} {...props} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Incorrect (useContext in React 19):**
|
||||
|
||||
```tsx
|
||||
const value = useContext(MyContext)
|
||||
const value = useContext(MyContext);
|
||||
```
|
||||
|
||||
**Correct (use instead of useContext):**
|
||||
|
||||
```tsx
|
||||
const value = use(MyContext)
|
||||
const value = use(MyContext);
|
||||
```
|
||||
|
||||
`use()` can also be called conditionally, unlike `useContext()`.
|
||||
|
||||
+26
-26
@@ -20,8 +20,8 @@ dependency-injectable.
|
||||
```tsx
|
||||
function ComposerInput() {
|
||||
// Tightly coupled to a specific hook
|
||||
const { input, setInput } = useChannelComposerState()
|
||||
return <TextInput value={input} onChangeText={setInput} />
|
||||
const { input, setInput } = useChannelComposerState();
|
||||
return <TextInput value={input} onChangeText={setInput} />;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -30,27 +30,27 @@ function ComposerInput() {
|
||||
```tsx
|
||||
// Define a GENERIC interface that any provider can implement
|
||||
interface ComposerState {
|
||||
input: string
|
||||
attachments: Attachment[]
|
||||
isSubmitting: boolean
|
||||
input: string;
|
||||
attachments: Attachment[];
|
||||
isSubmitting: boolean;
|
||||
}
|
||||
|
||||
interface ComposerActions {
|
||||
update: (updater: (state: ComposerState) => ComposerState) => void
|
||||
submit: () => void
|
||||
update: (updater: (state: ComposerState) => ComposerState) => void;
|
||||
submit: () => void;
|
||||
}
|
||||
|
||||
interface ComposerMeta {
|
||||
inputRef: React.RefObject<TextInput>
|
||||
inputRef: React.RefObject<TextInput>;
|
||||
}
|
||||
|
||||
interface ComposerContextValue {
|
||||
state: ComposerState
|
||||
actions: ComposerActions
|
||||
meta: ComposerMeta
|
||||
state: ComposerState;
|
||||
actions: ComposerActions;
|
||||
meta: ComposerMeta;
|
||||
}
|
||||
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null)
|
||||
const ComposerContext = createContext<ComposerContextValue | null>(null);
|
||||
```
|
||||
|
||||
**UI components consume the interface, not the implementation:**
|
||||
@@ -61,7 +61,7 @@ function ComposerInput() {
|
||||
state,
|
||||
actions: { update },
|
||||
meta,
|
||||
} = use(ComposerContext)
|
||||
} = use(ComposerContext);
|
||||
|
||||
// This component works with ANY provider that implements the interface
|
||||
return (
|
||||
@@ -70,7 +70,7 @@ function ComposerInput() {
|
||||
value={state.input}
|
||||
onChangeText={(text) => update((s) => ({ ...s, input: text }))}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -79,9 +79,9 @@ function ComposerInput() {
|
||||
```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()
|
||||
const [state, setState] = useState(initialState);
|
||||
const inputRef = useRef(null);
|
||||
const submit = useForwardMessage();
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
@@ -93,13 +93,13 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Provider B: Global synced state for channels
|
||||
function ChannelProvider({ channelId, children }: Props) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId)
|
||||
const inputRef = useRef(null)
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<ComposerContext
|
||||
@@ -111,7 +111,7 @@ function ChannelProvider({ channelId, children }: Props) {
|
||||
>
|
||||
{children}
|
||||
</ComposerContext>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -165,21 +165,21 @@ function ForwardMessageDialog() {
|
||||
</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>
|
||||
} = 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} />
|
||||
const { state } = use(ComposerContext);
|
||||
return <Preview message={state.input} attachments={state.attachments} />;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+18
-28
@@ -16,18 +16,15 @@ useState, Zustand, or a server sync.
|
||||
```tsx
|
||||
function ChannelComposer({ channelId }: { channelId: string }) {
|
||||
// UI component knows about global state implementation
|
||||
const state = useGlobalChannelState(channelId)
|
||||
const { submit, updateInput } = useChannelSync(channelId)
|
||||
const state = useGlobalChannelState(channelId);
|
||||
const { submit, updateInput } = useChannelSync(channelId);
|
||||
|
||||
return (
|
||||
<Composer.Frame>
|
||||
<Composer.Input
|
||||
value={state.input}
|
||||
onChange={(text) => sync.updateInput(text)}
|
||||
/>
|
||||
<Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />
|
||||
<Composer.Submit onPress={() => sync.submit()} />
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -39,21 +36,17 @@ function ChannelProvider({
|
||||
channelId,
|
||||
children,
|
||||
}: {
|
||||
channelId: string
|
||||
children: React.ReactNode
|
||||
channelId: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { state, update, submit } = useGlobalChannel(channelId)
|
||||
const inputRef = useRef(null)
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
state={state}
|
||||
actions={{ update, submit }}
|
||||
meta={{ inputRef }}
|
||||
>
|
||||
<Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// UI component only knows about the context interface
|
||||
@@ -66,7 +59,7 @@ function ChannelComposer() {
|
||||
<Composer.Submit />
|
||||
</Composer.Footer>
|
||||
</Composer.Frame>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
@@ -75,7 +68,7 @@ function Channel({ channelId }: { channelId: string }) {
|
||||
<ChannelProvider channelId={channelId}>
|
||||
<ChannelComposer />
|
||||
</ChannelProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -84,28 +77,25 @@ function Channel({ channelId }: { channelId: string }) {
|
||||
```tsx
|
||||
// Local state for ephemeral forms
|
||||
function ForwardMessageProvider({ children }) {
|
||||
const [state, setState] = useState(initialState)
|
||||
const forwardMessage = useForwardMessage()
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
state={state}
|
||||
actions={{ update: setState, submit: forwardMessage }}
|
||||
>
|
||||
<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)
|
||||
const { state, update, submit } = useGlobalChannel(channelId);
|
||||
|
||||
return (
|
||||
<Composer.Provider state={state} actions={{ update, submit }}>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+18
-18
@@ -15,15 +15,15 @@ or awkward refs.
|
||||
|
||||
```tsx
|
||||
function ForwardMessageComposer() {
|
||||
const [state, setState] = useState(initialState)
|
||||
const forwardMessage = useForwardMessage()
|
||||
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?
|
||||
@@ -37,7 +37,7 @@ function ForwardMessageDialog() {
|
||||
<ForwardButton /> {/* Needs to call submit */}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -45,20 +45,20 @@ function ForwardMessageDialog() {
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const [input, setInput] = useState('')
|
||||
const [input, setInput] = useState('');
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer onInputChange={setInput} />
|
||||
<MessagePreview input={input} />
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageComposer({ onInputChange }) {
|
||||
const [state, setState] = useState(initialState)
|
||||
const [state, setState] = useState(initialState);
|
||||
useEffect(() => {
|
||||
onInputChange(state.input) // Sync on every change 😬
|
||||
}, [state.input])
|
||||
onInputChange(state.input); // Sync on every change 😬
|
||||
}, [state.input]);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -66,13 +66,13 @@ function ForwardMessageComposer({ onInputChange }) {
|
||||
|
||||
```tsx
|
||||
function ForwardMessageDialog() {
|
||||
const stateRef = useRef(null)
|
||||
const stateRef = useRef(null);
|
||||
return (
|
||||
<Dialog>
|
||||
<ForwardMessageComposer stateRef={stateRef} />
|
||||
<ForwardButton onPress={() => submit(stateRef.current)} />
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -80,9 +80,9 @@ function ForwardMessageDialog() {
|
||||
|
||||
```tsx
|
||||
function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState(initialState)
|
||||
const forwardMessage = useForwardMessage()
|
||||
const inputRef = useRef(null)
|
||||
const [state, setState] = useState(initialState);
|
||||
const forwardMessage = useForwardMessage();
|
||||
const inputRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Composer.Provider
|
||||
@@ -92,7 +92,7 @@ function ForwardMessageProvider({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
{children}
|
||||
</Composer.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardMessageDialog() {
|
||||
@@ -107,12 +107,12 @@ function ForwardMessageDialog() {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</ForwardMessageProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardButton() {
|
||||
const { actions } = use(Composer.Context)
|
||||
return <Button onPress={actions.submit}>Forward</Button>
|
||||
const { actions } = use(Composer.Context);
|
||||
return <Button onPress={actions.submit}>Forward</Button>;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user