Refactor Gemini model display: filter old versions, group low/high pкo, and improve naming (#114)

* Refactor Gemini model display: filter old versions, group low/high pro, and improve naming

* Fix potential negative minutes and refine Gemini 3 Pro grouping logic based on PR feedback

* The account change process has been sped up

* Address PR review: enhance error message and parameterize polling interval in process handler

* Address PR review: fix potential race condition in database backup logic

* fix: prevent renderer white screen and harden sentry gating

* feat:  localize model labels

* chore: enable hoisted node-linker

* chore: ignore .agent/.claude/.codex in prettier

---------

Co-authored-by: Draculabo <[email protected]>
This commit is contained in:
AngeloooooandDraculabo authored and GitHub committed 2026-02-19 19:37:30 +08:00
1 parent b9497648c6
commit 8ed4593f73
162 files changed
+2981 -2541

No files matched your search

@@ -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
@@ -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>
)
);
}
```
@@ -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:**
@@ -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.
@@ -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>
)
);
}
```
@@ -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()`.
@@ -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} />;
}
```
@@ -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>
)
);
}
```
@@ -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>;
}
```