Skip to content

fix(admin): keep hook order stable on content list when a refetch errors (#1415)#1570

Open
marcusbellamyshaw-cell wants to merge 1 commit into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1415-contentlist-hook-order
Open

fix(admin): keep hook order stable on content list when a refetch errors (#1415)#1570
marcusbellamyshaw-cell wants to merge 1 commit into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1415-contentlist-hook-order

Conversation

@marcusbellamyshaw-cell

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a crash (React #300 "Rendered fewer hooks than expected") on the content list page when an action that triggers a refetch errors.

ContentListPage declared the onLoadMore React.useCallback inline in JSX, below three early returns (!manifest, !collectionConfig, error). On the render that takes the error guard, that hook never runs, so the render executes one fewer hook than a successful render — which is exactly what React #300 forbids. Any refetch that fails (e.g. a transient API error after the list has loaded) crashed the whole page instead of showing the error state.

Fix: lift the callback above the early returns so the hook count is identical on every render path. Added a comment documenting the invariant.

Closes #1415.

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • I have added a changeset (if this PR changes a published package)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 ultracode

Screenshots / test output

New test in packages/admin/tests/router.test.tsx: renders the page, swaps the content handler to a 500, refetches, and asserts the Retry button renders (the page no longer crashes). Targeted admin + core suites green; typecheck/lint/format clean.

…eturns (emdash-cms#1415)

The inline `React.useCallback` passed to `onLoadMore` sat below the
`!manifest` / `!collectionConfig` / `error` guards. A render that took one
of those early returns (e.g. when a sort change or fast navigation triggers
a refetch that errors) ran one fewer hook than the preceding full render,
tripping React emdash-cms#300 "Rendered fewer hooks than expected" — the same class
as emdash-cms#439 / emdash-cms#467, which lifted other hooks above the guards.

Lift the callback to `handleLoadMore` alongside the `items` useMemo so every
render runs the same hooks regardless of which branch it takes.

Adds a regression test driving a content refetch into an error after a
successful render and asserting the ErrorScreen renders instead of crashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c8b09c8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach

This is the right fix for a real bug, done the idiomatic way. ContentListPage declared onLoadMore={React.useCallback(...)} inline in its JSX, below three early returns (!manifest, !collectionConfig, error). A render that took a guard therefore ran one fewer hook than a full render → React #300 "Rendered fewer hooks than expected." Hoisting the useCallback above the guards is the canonical fix, and the added comment documents the invariant. Scope is correctly limited to ContentListPage (#1415); no drive-by edits.

What I verified

  • The hook baseline is real. ContentListPage is a child of adminLayoutRoute, whose RootComponent fetches the shared ["manifest"] query and only renders <Shell><Outlet/></Shell> once it resolves (router.tsx:243-265). So on ContentListPage's first mount the manifest is already cached → !manifest/!collectionConfig/error are all false → the first render reaches the JSX and establishes the full hook count. A later erroring refetch then takes the error guard; in the buggy version that skipped the inline useCallback#300. The fix makes every render path run the same hooks. Confirmed correct.
  • The fix is complete. Reading the full function (lines 297–534), after this change no hooks remain below the early returns — only const collectionConfig, the handleLocaleChange arrow, and the JSX return. Nothing else was missed.
  • The test reproduces the crash. It waits for the "Add New" link (proving the full-hook baseline was established on a successful render), swaps the content handler to a 500, forces queryClient.refetchQueries, and asserts the Retry button renders. React Query v5.90 sets error on a failed refetch even when cached data exists, so the if (error) guard fires — in the buggy version that render throws #300 and the Retry button never appears. The screen.getByRole(...)await expect.element(...) query pattern matches the existing locale tests in the same file (no findByRole is used anywhere in the suite), so it's consistent with the codebase's waiting idiom.
  • No recurrence elsewhere. Grepped for inline useCallback-in-JSX across packages/admin/src; this was the only instance.
  • Conventions. Tabs, no new user-facing strings (Lingui/RTL N/A), no SQL/API changes. Changeset targets @emdash-cms/admin patch — correct.

Conclusion

Implementation is clean and correct, with a valid regression test. The only thing worth a look is one line of changeset prose that drifts into implementation mechanics — non-blocking.

"@emdash-cms/admin": patch
---

Fixes a crash on the content list when an action that refetches (changing the sort, fast navigation) coincides with a load error. The `onLoadMore` callback hook now sits above the page's early returns, so an error render runs the same number of hooks as a normal render instead of throwing React error #300. Closes #1415.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Per AGENTS.md, a changeset is release notes for someone upgrading — lead with the observable effect and leave out internal mechanics (file names, refactors, how you implemented it). The middle sentence here ("The onLoadMore callback hook now sits above the page's early returns, so an error render runs the same number of hooks as a normal render instead of throwing React error #300") is exactly the implementation detail a user reading the changelog doesn't need. The first sentence already states the observable effect; the mechanics sentence can go.

Suggested change
Fixes a crash on the content list when an action that refetches (changing the sort, fast navigation) coincides with a load error. The `onLoadMore` callback hook now sits above the page's early returns, so an error render runs the same number of hooks as a normal render instead of throwing React error #300. Closes #1415.
Fixes a crash on the content list when an action that refetches (changing the sort, fast navigation) coincides with a load error. Closes #1415.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin: ContentListPage still throws React #300 (hook-order) — inline useCallback for onLoadMore sits below early returns

1 participant