enable React Compiler and add Oxc tooling - #26
Conversation
|
@Priyansh4444 is attempting to deploy a commit to the pcstyle Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Will be a huge pr if considering oxc tooling. |
There was a problem hiding this comment.
14 issues found across 155 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/flashExperiments/ExperimentDiscussion.tsx">
<violation number="1" location="src/flashExperiments/ExperimentDiscussion.tsx:264">
P2: Opening a nested reply composer does not focus its textarea because `focusBodyOnMount` is enabled without supplying the ref that `ComposerShell` uses for focusing. Wiring a `useRef<HTMLTextAreaElement>` through this local `Composer` as `textareaRef` would restore the intended reply flow.</violation>
</file>
<file name="src/components/AttachmentGallery.tsx">
<violation number="1" location="src/components/AttachmentGallery.tsx:40">
P2: The new `<track kind="captions" src="data:text/vtt,WEBVTT" />` advertises captions that don't exist. It points at an empty WebVTT file (zero cues), so users who enable the CC control get nothing, and since it isn't marked `default` it's never even selected at runtime. If the goal was to genuinely support captions, ship a real caption source (with `srcLang`/`label`); if captions aren't available, drop the track rather than claiming caption availability that the video doesn't deliver — an empty captions track is an accessibility anti-pattern that misrepresents the video's accessibility.</violation>
</file>
<file name="scripts/linear-pull.mjs">
<violation number="1" location="scripts/linear-pull.mjs:147">
P3: The comment `// Build issue lookup for dependency resolution` now sits above nothing — the `issueMap` it described was removed in this diff, so it's orphaned and misleading. Drop the stale comment line.</violation>
</file>
<file name="src/routes/redesign/RedesignPostPage.tsx">
<violation number="1" location="src/routes/redesign/RedesignPostPage.tsx:253">
P3: Sidebar resizing no longer exposes current width or bounds to assistive technology: this button only announces a label while ArrowLeft/ArrowRight change the value. Restore a resize/slider semantic with value and range ARIA properties rather than presenting it as an action button.</violation>
</file>
<file name="src/components/PostForm.tsx">
<violation number="1" location="src/components/PostForm.tsx:246">
P2: Wall-post dialog now opens with focus on its close button instead of the title field because this no longer renders the `autofocus` attribute Dialog uses for fallback focus. Preserve an autofocus marker or provide the wall dialog's title ref to `Dialog.initialFocusRef`.</violation>
</file>
<file name="src/components/ComposerShell.tsx">
<violation number="1" location="src/components/ComposerShell.tsx:119">
P2: The Cmd/Ctrl+Enter fallback can insert a newline into the textarea because this branch requests the form submission without preventing the original key event; the callback branch already suppresses it. Prevent the default event before calling `requestSubmit()` so the keyboard shortcut submits without editing the draft.</violation>
</file>
<file name="src/components/Composer.tsx">
<violation number="1" location="src/components/Composer.tsx:131">
P2: After a failed reply, editing the draft leaves the old error alert visible. The direct setter removed the previous on-change reset, so the submission error should be cleared when the draft changes.</violation>
<violation number="2" location="src/components/Composer.tsx:193">
P1: Pressing Cmd/Ctrl+Enter repeatedly while a reply is sending can create duplicate replies and agent tasks. The native form path bypasses the old `busy` guard, so the shortcut should be prevented while `isSubmitting` or the action should have an equivalent submission guard.</violation>
</file>
<file name="src/components/RichEmbedList.tsx">
<violation number="1" location="src/components/RichEmbedList.tsx:59">
P3: Every rich-embed render now reparses and rebuilds the body’s previews in addition to the wrapper’s memoized calculation, and the duplicated logic can drift later. A shared `buildPreviews(text)` helper or passing the computed previews into `RichEmbedListView` would keep one derivation and avoid the extra scan.</violation>
<violation number="2" location="src/components/RichEmbedList.tsx:110">
P2: Direct video previews advertise a captions track that can never display any captions, so users who need captions still have no accessible equivalent for videos with speech. Real per-video WebVTT should be supplied when available; otherwise this empty track should not be treated as caption support.</violation>
</file>
<file name="package.json">
<violation number="1" location="package.json:53">
P3: The new @types/babel__core (^7.20.5) is bumped a major behind the added @babel/core (^8.0.1); Babel 8 has no matching ⁄types release, and nothing in the source imports either package (only @rolldown/plugin-babel, which brings its own types). Consider dropping the @types/babel__core devDependency so the types don't mislead against the v8 runtime.</violation>
</file>
<file name="src/components/UserSwitcher.tsx">
<violation number="1" location="src/components/UserSwitcher.tsx:46">
P2: Pressing Escape closes the switcher but leaves focus on the unmounted selected-user button, typically dropping focus to `body` instead of returning it to the trigger. The new `<dialog>` removed the only Escape path that called `triggerRef.current?.focus()`; retain that restoration for keyboard dismissal.</violation>
</file>
<file name="convex/orgs.test.ts">
<violation number="1" location="convex/orgs.test.ts:30">
P3: The removed second argument to `expect(...)` was a per-iteration diagnostic message. Now if any slug in the loop fails (`ab`, `-acme`, `acme-`, `Acme`, `acme_team`, `postwork`, `staging`), the assertion failure no longer names which slug caused it, so debugging a regression requires re-running with added logging. Consider keeping a custom message (e.g. `expect(orgSlugError(slug), slug).not.toBeNull()`) or otherwise including the slug in the assertion output.</violation>
</file>
<file name="src/components/Skeleton.tsx">
<violation number="1" location="src/components/Skeleton.tsx:13">
P3: `<output>` cannot contain a block-level `<div>` (its content model is phrasing content), so nesting the skeleton `<div aria-hidden>` grid inside it is invalid HTML. Since the original intent was an announcer (role="status"), either keep `role="status"` on a block element or split the layout: put the text in the `<output>` and the grid in a separate flow element outside it.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| onSubmit={() => void submit()} | ||
| submitting={isSubmitting} | ||
| disabled={isSubmitting || !body.trim() || hasUploading || hasAttachmentErrors} | ||
| submitType="submit" |
There was a problem hiding this comment.
P1: Pressing Cmd/Ctrl+Enter repeatedly while a reply is sending can create duplicate replies and agent tasks. The native form path bypasses the old busy guard, so the shortcut should be prevented while isSubmitting or the action should have an equivalent submission guard.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/Composer.tsx, line 193:
<comment>Pressing Cmd/Ctrl+Enter repeatedly while a reply is sending can create duplicate replies and agent tasks. The native form path bypasses the old `busy` guard, so the shortcut should be prevented while `isSubmitting` or the action should have an equivalent submission guard.</comment>
<file context>
@@ -187,27 +173,31 @@ export function Composer({
- onSubmit={() => void submit()}
+ submitting={isSubmitting}
+ disabled={isSubmitting || !body.trim() || hasUploading || hasAttachmentErrors}
+ submitType="submit"
/>
- {error ? <p role="alert" className="ui-error mt-2">{error}</p> : null}
</file context>
| body={body} | ||
| setBody={setBody} | ||
| autoFocus={autoFocus} | ||
| focusBodyOnMount={focusBodyOnMount} |
There was a problem hiding this comment.
P2: Opening a nested reply composer does not focus its textarea because focusBodyOnMount is enabled without supplying the ref that ComposerShell uses for focusing. Wiring a useRef<HTMLTextAreaElement> through this local Composer as textareaRef would restore the intended reply flow.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/flashExperiments/ExperimentDiscussion.tsx, line 264:
<comment>Opening a nested reply composer does not focus its textarea because `focusBodyOnMount` is enabled without supplying the ref that `ComposerShell` uses for focusing. Wiring a `useRef<HTMLTextAreaElement>` through this local `Composer` as `textareaRef` would restore the intended reply flow.</comment>
<file context>
@@ -276,7 +261,7 @@ function Composer({
body={body}
setBody={setBody}
- autoFocus={autoFocus}
+ focusBodyOnMount={focusBodyOnMount}
placeholder={placeholder}
rows={2}
</file context>
| className="ui-media-outline max-h-80 max-w-full rounded-md bg-black" | ||
| > | ||
| <source src={attachment.url} type={attachment.contentType} /> | ||
| <track kind="captions" src="data:text/vtt,WEBVTT" /> |
There was a problem hiding this comment.
P2: The new <track kind="captions" src="data:text/vtt,WEBVTT" /> advertises captions that don't exist. It points at an empty WebVTT file (zero cues), so users who enable the CC control get nothing, and since it isn't marked default it's never even selected at runtime. If the goal was to genuinely support captions, ship a real caption source (with srcLang/label); if captions aren't available, drop the track rather than claiming caption availability that the video doesn't deliver — an empty captions track is an accessibility anti-pattern that misrepresents the video's accessibility.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/AttachmentGallery.tsx, line 40:
<comment>The new `<track kind="captions" src="data:text/vtt,WEBVTT" />` advertises captions that don't exist. It points at an empty WebVTT file (zero cues), so users who enable the CC control get nothing, and since it isn't marked `default` it's never even selected at runtime. If the goal was to genuinely support captions, ship a real caption source (with `srcLang`/`label`); if captions aren't available, drop the track rather than claiming caption availability that the video doesn't deliver — an empty captions track is an accessibility anti-pattern that misrepresents the video's accessibility.</comment>
<file context>
@@ -47,6 +37,7 @@ export function AttachmentMedia({
className="ui-media-outline max-h-80 max-w-full rounded-md bg-black"
>
<source src={attachment.url} type={attachment.contentType} />
+ <track kind="captions" src="data:text/vtt,WEBVTT" />
<a href={attachment.url}>open {attachment.filename}</a>
</video>
</file context>
| titleError={titleTouched && titleMissing ? "add a title." : undefined} | ||
| titlePlaceholder={titlePlaceholder} | ||
| titleAutoFocus={autoFocusTitle} | ||
| focusTitleOnMount={autoFocusTitle} |
There was a problem hiding this comment.
P2: Wall-post dialog now opens with focus on its close button instead of the title field because this no longer renders the autofocus attribute Dialog uses for fallback focus. Preserve an autofocus marker or provide the wall dialog's title ref to Dialog.initialFocusRef.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/PostForm.tsx, line 246:
<comment>Wall-post dialog now opens with focus on its close button instead of the title field because this no longer renders the `autofocus` attribute Dialog uses for fallback focus. Preserve an autofocus marker or provide the wall dialog's title ref to `Dialog.initialFocusRef`.</comment>
<file context>
@@ -263,7 +243,7 @@ export function PostForm({
titleError={titleTouched && titleMissing ? "add a title." : undefined}
titlePlaceholder={titlePlaceholder}
- titleAutoFocus={autoFocusTitle}
+ focusTitleOnMount={autoFocusTitle}
titleRequired={requireTitle}
titleOptional={!requireTitle}
</file context>
| event.preventDefault(); | ||
| onSubmit(); | ||
| } else { | ||
| event.currentTarget.form?.requestSubmit(); |
There was a problem hiding this comment.
P2: The Cmd/Ctrl+Enter fallback can insert a newline into the textarea because this branch requests the form submission without preventing the original key event; the callback branch already suppresses it. Prevent the default event before calling requestSubmit() so the keyboard shortcut submits without editing the draft.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/ComposerShell.tsx, line 119:
<comment>The Cmd/Ctrl+Enter fallback can insert a newline into the textarea because this branch requests the form submission without preventing the original key event; the callback branch already suppresses it. Prevent the default event before calling `requestSubmit()` so the keyboard shortcut submits without editing the draft.</comment>
<file context>
@@ -103,14 +108,16 @@ export function ComposerShell({
+ event.preventDefault();
+ onSubmit();
+ } else {
+ event.currentTarget.form?.requestSubmit();
+ }
}
</file context>
| event.currentTarget.form?.requestSubmit(); | |
| event.preventDefault(); | |
| event.currentTarget.form?.requestSubmit(); |
|
|
||
| <aside aria-label="Agent panels" className="relative hidden min-w-0 xl:block"> | ||
| {sidebarOpen ? ( | ||
| <button |
There was a problem hiding this comment.
P3: Sidebar resizing no longer exposes current width or bounds to assistive technology: this button only announces a label while ArrowLeft/ArrowRight change the value. Restore a resize/slider semantic with value and range ARIA properties rather than presenting it as an action button.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/redesign/RedesignPostPage.tsx, line 253:
<comment>Sidebar resizing no longer exposes current width or bounds to assistive technology: this button only announces a label while ArrowLeft/ArrowRight change the value. Restore a resize/slider semantic with value and range ARIA properties rather than presenting it as an action button.</comment>
<file context>
@@ -162,225 +162,209 @@ export function RedesignPostPage() {
+
+ <aside aria-label="Agent panels" className="relative hidden min-w-0 xl:block">
+ {sidebarOpen ? (
+ <button
+ type="button"
+ aria-label="Resize agent sidebar"
</file context>
| text: string; | ||
| storedPreviews?: FunctionReturnType<typeof api.linkPreviews.get>; | ||
| }) { | ||
| const previews = [ |
There was a problem hiding this comment.
P3: Every rich-embed render now reparses and rebuilds the body’s previews in addition to the wrapper’s memoized calculation, and the duplicated logic can drift later. A shared buildPreviews(text) helper or passing the computed previews into RichEmbedListView would keep one derivation and avoid the extra scan.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/RichEmbedList.tsx, line 59:
<comment>Every rich-embed render now reparses and rebuilds the body’s previews in addition to the wrapper’s memoized calculation, and the duplicated logic can drift later. A shared `buildPreviews(text)` helper or passing the computed previews into `RichEmbedListView` would keep one derivation and avoid the extra scan.</comment>
<file context>
@@ -35,9 +46,27 @@ export function RichEmbedList({ text }: { text: string }) {
+ text: string;
+ storedPreviews?: FunctionReturnType<typeof api.linkPreviews.get>;
+}) {
+ const previews = [
+ ...new Map(
+ extractUrls(text)
</file context>
| "@edge-runtime/vm": "^5.0.0", | ||
| "@rolldown/plugin-babel": "^0.2.3", | ||
| "@tailwindcss/vite": "^4.3.1", | ||
| "@types/babel__core": "^7.20.5", |
There was a problem hiding this comment.
P3: The new @types/babel__core (^7.20.5) is bumped a major behind the added @babel/core (^8.0.1); Babel 8 has no matching ⁄types release, and nothing in the source imports either package (only @rolldown/plugin-babel, which brings its own types). Consider dropping the @types/babel__core devDependency so the types don't mislead against the v8 runtime.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At package.json, line 53:
<comment>The new @types/babel__core (^7.20.5) is bumped a major behind the added @babel/core (^8.0.1); Babel 8 has no matching ⁄types release, and nothing in the source imports either package (only @rolldown/plugin-babel, which brings its own types). Consider dropping the @types/babel__core devDependency so the types don't mislead against the v8 runtime.</comment>
<file context>
@@ -36,25 +40,31 @@
"@edge-runtime/vm": "^5.0.0",
+ "@rolldown/plugin-babel": "^0.2.3",
"@tailwindcss/vite": "^4.3.1",
+ "@types/babel__core": "^7.20.5",
"@types/bun": "^1.3.14",
"@types/node": "^26.0.0",
</file context>
| expect(orgSlugError("acme-2")).toBeNull(); | ||
| for (const slug of ["ab", "-acme", "acme-", "Acme", "acme_team", "postwork", "staging"]) { | ||
| expect(orgSlugError(slug), slug).not.toBeNull(); | ||
| expect(orgSlugError(slug)).not.toBeNull(); |
There was a problem hiding this comment.
P3: The removed second argument to expect(...) was a per-iteration diagnostic message. Now if any slug in the loop fails (ab, -acme, acme-, Acme, acme_team, postwork, staging), the assertion failure no longer names which slug caused it, so debugging a regression requires re-running with added logging. Consider keeping a custom message (e.g. expect(orgSlugError(slug), slug).not.toBeNull()) or otherwise including the slug in the assertion output.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At convex/orgs.test.ts, line 30:
<comment>The removed second argument to `expect(...)` was a per-iteration diagnostic message. Now if any slug in the loop fails (`ab`, `-acme`, `acme-`, `Acme`, `acme_team`, `postwork`, `staging`), the assertion failure no longer names which slug caused it, so debugging a regression requires re-running with added logging. Consider keeping a custom message (e.g. `expect(orgSlugError(slug), slug).not.toBeNull()`) or otherwise including the slug in the assertion output.</comment>
<file context>
@@ -27,7 +27,7 @@ describe("organization slugs", () => {
expect(orgSlugError("acme-2")).toBeNull();
for (const slug of ["ab", "-acme", "acme-", "Acme", "acme_team", "postwork", "staging"]) {
- expect(orgSlugError(slug), slug).not.toBeNull();
+ expect(orgSlugError(slug)).not.toBeNull();
}
});
</file context>
| }) { | ||
| return ( | ||
| <div role="status" aria-label={label} className="w-full"> | ||
| <output aria-label={label} className="block w-full"> |
There was a problem hiding this comment.
P3: <output> cannot contain a block-level <div> (its content model is phrasing content), so nesting the skeleton <div aria-hidden> grid inside it is invalid HTML. Since the original intent was an announcer (role="status"), either keep role="status" on a block element or split the layout: put the text in the <output> and the grid in a separate flow element outside it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/Skeleton.tsx, line 13:
<comment>`<output>` cannot contain a block-level `<div>` (its content model is phrasing content), so nesting the skeleton `<div aria-hidden>` grid inside it is invalid HTML. Since the original intent was an announcer (role="status"), either keep `role="status"` on a block element or split the layout: put the text in the `<output>` and the grid in a separate flow element outside it.</comment>
<file context>
@@ -15,14 +10,14 @@ export function Skeleton({
}) {
return (
- <div role="status" aria-label={label} className="w-full">
+ <output aria-label={label} className="block w-full">
<span className="sr-only">{label}</span>
<div aria-hidden="true" className="grid gap-3">
</file context>
There was a problem hiding this comment.
5 issues found across 16 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/routes/SpacesPage.tsx">
<violation number="1" location="src/routes/SpacesPage.tsx:124">
P1: Closing the dialog while creation is pending still creates the space and then redirects to it, even though the user cancelled the flow. Keep the dialog/cancel controls non-dismissible while the action is pending (use `useActionState`'s third pending value).</violation>
</file>
<file name="src/components/ProfileSettingsForm.tsx">
<violation number="1" location="src/components/ProfileSettingsForm.tsx:100">
P2: A failed image upload remains displayed after a later save because the action never clears local `error`, and it masks `submitState.error`; clear the upload error when save submission starts.</violation>
</file>
<file name="src/components/Button.tsx">
<violation number="1" location="src/components/Button.tsx:74">
P2: Passing `loading` to `FormSubmitButton` is ignored whenever its form is idle, so the button is neither rendered as loading nor disabled for caller-controlled async work; preserve the explicit `ButtonProps.loading` value (or omit that prop from this wrapper's public type).</violation>
</file>
<file name="src/components/PostForm.tsx">
<violation number="1" location="src/components/PostForm.tsx:99">
P3: Removing the useMemo here makes `readDraft` run on every render, parsing JSON from sessionStorage on each keystroke. Unlike the pure `SPACES`/`spaces` computations (which the React Compiler re-memoizes automatically), `readDraft` reads impure external storage, so the compiler won't optimize it away. Since the result only seeds useState initializers, consider keeping it memoized with `useMemo(() => readDraft(draftKey), [draftKey])` to avoid the per-render sessionStorage read.</violation>
<violation number="2" location="src/components/PostForm.tsx:137">
P3: The space-options derivation is now duplicated: the render computes `spaceOptions` (line 129) and the useEffect rebuilds the identical array as `options` (line 137) to keep the dependency array on primitives instead of the recreated `spaceOptions`. This is done so the effect doesn't depend on a fresh-array identity each render, but the two copies can silently drift if one is edited and the other isn't, which could make the effect select a space key inconsistent with what the dropdown shows. Consider extracting a single helper that both the render and the effect call (e.g. a module-level `buildSpaceOptions(fixedSpace, spaces)`), so the two are guaranteed to stay in sync.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| onClose={onClose} | ||
| initialFocusRef={nameRef} | ||
| dismissible={!saving} | ||
| dismissible |
There was a problem hiding this comment.
P1: Closing the dialog while creation is pending still creates the space and then redirects to it, even though the user cancelled the flow. Keep the dialog/cancel controls non-dismissible while the action is pending (use useActionState's third pending value).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/SpacesPage.tsx, line 124:
<comment>Closing the dialog while creation is pending still creates the space and then redirects to it, even though the user cancelled the flow. Keep the dialog/cancel controls non-dismissible while the action is pending (use `useActionState`'s third pending value).</comment>
<file context>
@@ -89,84 +89,75 @@ function CreateSpaceDialog({ onClose }: { onClose: () => void }) {
onClose={onClose}
initialFocusRef={nameRef}
- dismissible={!saving}
+ dismissible
>
- <form className="grid gap-4" onSubmit={submit}>
</file context>
| } | ||
| }; | ||
| const [submitState, submitAction] = useActionState( | ||
| async (_previous: { error: string | null; saved: boolean }, formData: FormData) => { |
There was a problem hiding this comment.
P2: A failed image upload remains displayed after a later save because the action never clears local error, and it masks submitState.error; clear the upload error when save submission starts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/ProfileSettingsForm.tsx, line 100:
<comment>A failed image upload remains displayed after a later save because the action never clears local `error`, and it masks `submitState.error`; clear the upload error when save submission starts.</comment>
<file context>
@@ -100,41 +96,41 @@ export function ProfileSettingsForm() {
- }
- };
+ const [submitState, submitAction] = useActionState(
+ async (_previous: { error: string | null; saved: boolean }, formData: FormData) => {
+ const nextName = String(formData.get("name") ?? "").trim();
+ const nextTitle = String(formData.get("title") ?? "").trim();
</file context>
| async (_previous: { error: string | null; saved: boolean }, formData: FormData) => { | |
| async (_previous: { error: string | null; saved: boolean }, formData: FormData) => { | |
| setError(null); |
| export function FormSubmitButton({ loadingLabel = "working…", children, ...props }: ButtonProps) { | ||
| const { pending } = useFormStatus(); | ||
| return ( | ||
| <Button {...props} type="submit" loading={pending} loadingLabel={loadingLabel}> |
There was a problem hiding this comment.
P2: Passing loading to FormSubmitButton is ignored whenever its form is idle, so the button is neither rendered as loading nor disabled for caller-controlled async work; preserve the explicit ButtonProps.loading value (or omit that prop from this wrapper's public type).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/Button.tsx, line 74:
<comment>Passing `loading` to `FormSubmitButton` is ignored whenever its form is idle, so the button is neither rendered as loading nor disabled for caller-controlled async work; preserve the explicit `ButtonProps.loading` value (or omit that prop from this wrapper's public type).</comment>
<file context>
@@ -66,3 +67,12 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
+export function FormSubmitButton({ loadingLabel = "working…", children, ...props }: ButtonProps) {
+ const { pending } = useFormStatus();
+ return (
+ <Button {...props} type="submit" loading={pending} loadingLabel={loadingLabel}>
+ {children}
+ </Button>
</file context>
| <Button {...props} type="submit" loading={pending} loadingLabel={loadingLabel}> | |
| <Button {...props} type="submit" loading={props.loading || pending} loadingLabel={loadingLabel}> |
| onSubmit: (fields: PostFormFields) => Promise<void> | void; | ||
| }) { | ||
| const draft = useMemo(() => readDraft(draftKey), [draftKey]); | ||
| const draft = readDraft(draftKey); |
There was a problem hiding this comment.
P3: Removing the useMemo here makes readDraft run on every render, parsing JSON from sessionStorage on each keystroke. Unlike the pure SPACES/spaces computations (which the React Compiler re-memoizes automatically), readDraft reads impure external storage, so the compiler won't optimize it away. Since the result only seeds useState initializers, consider keeping it memoized with useMemo(() => readDraft(draftKey), [draftKey]) to avoid the per-render sessionStorage read.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/PostForm.tsx, line 99:
<comment>Removing the useMemo here makes `readDraft` run on every render, parsing JSON from sessionStorage on each keystroke. Unlike the pure `SPACES`/`spaces` computations (which the React Compiler re-memoizes automatically), `readDraft` reads impure external storage, so the compiler won't optimize it away. Since the result only seeds useState initializers, consider keeping it memoized with `useMemo(() => readDraft(draftKey), [draftKey])` to avoid the per-render sessionStorage read.</comment>
<file context>
@@ -96,7 +96,7 @@ export function PostForm({
onSubmit: (fields: PostFormFields) => Promise<void> | void;
}) {
- const draft = useMemo(() => readDraft(draftKey), [draftKey]);
+ const draft = readDraft(draftKey);
const spaces = useSpacesList();
const [title, setTitle] = useState(draft?.title ?? "");
</file context>
| ); | ||
| if (!hasMatch && spaceOptions[0]) { | ||
| setSpaceKey(spaceOptions[0].id ?? spaceOptions[0].label); | ||
| const options: SpaceOption[] = fixedSpace |
There was a problem hiding this comment.
P3: The space-options derivation is now duplicated: the render computes spaceOptions (line 129) and the useEffect rebuilds the identical array as options (line 137) to keep the dependency array on primitives instead of the recreated spaceOptions. This is done so the effect doesn't depend on a fresh-array identity each render, but the two copies can silently drift if one is edited and the other isn't, which could make the effect select a space key inconsistent with what the dropdown shows. Consider extracting a single helper that both the render and the effect call (e.g. a module-level buildSpaceOptions(fixedSpace, spaces)), so the two are guaranteed to stay in sync.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/PostForm.tsx, line 137:
<comment>The space-options derivation is now duplicated: the render computes `spaceOptions` (line 129) and the useEffect rebuilds the identical array as `options` (line 137) to keep the dependency array on primitives instead of the recreated `spaceOptions`. This is done so the effect doesn't depend on a fresh-array identity each render, but the two copies can silently drift if one is edited and the other isn't, which could make the effect select a space key inconsistent with what the dropdown shows. Consider extracting a single helper that both the render and the effect call (e.g. a module-level `buildSpaceOptions(fixedSpace, spaces)`), so the two are guaranteed to stay in sync.</comment>
<file context>
@@ -125,24 +125,25 @@ export function PostForm({
- const hasMatch = spaceOptions.some((option) => (option.id ?? option.label) === spaceKey);
- if (!hasMatch && spaceOptions[0]) {
- setSpaceKey(spaceOptions[0].id ?? spaceOptions[0].label);
+ const options: SpaceOption[] = fixedSpace
+ ? [fixedSpace]
+ : spaces.length > 0
</file context>
Summary
19.2.8and Vite to8.2.0.useMemo,useCallback, andmemousage so the compiler owns memoization.useActionState,useFormStatus, anduseOptimisticto reply, post-edit, space creation, profile save, and agent-task flows.1.77.0and Oxfmt0.62.0with repository scripts and configuration.Testing
bun run format:checkbun run lintbun run testbun run typecheckbun run buildbunx react-scan init --yes --skip-installAll checks pass: Oxfmt, Oxlint with zero diagnostics, 112 unit tests, 92 Convex tests, typecheck, and production build.