patternnextjsMajor
Server-rendered client editor loses all local state after every Server Action: key built from render time
Viewed 0 times
Next.js 15+ / React 19
revalidatePath remountkey Date.nowlost state after server actionadjust state on prop changehooks engine vitestrenderToStaticMarkup hybrid mock
ssrnodejsbrowser
Problem
A full-screen client editor rendered by an App Router page had key={
${id}-${revision}-${Date.now()}} so that every visit restarts from server data. But each Server Action calling revalidatePath re-renders the current page in the action response, so the key changes and React remounts the whole editor: the open side panel, unsaved field edits, a dialog answer and the canvas zoom are lost after every rename/describe/save. Child panels keyed with the revision were remounted too.Solution
Key the editor by the entity id only, and do not put the revision in child panel keys. Keep the document in local state and update it from each action's response. To pick up changes written elsewhere, use the "adjust state when a prop changes" pattern during render: remember the last server revision seen; when props bring a revision newer than both that and the local revision, copy the server document/name/state into local state without touching UI state (panel, zoom). Drop router.refresh() after actions that already revalidate. Test by calling the async page component and reading the returned element key twice (must be equal), and by mounting the editor with a small hooks engine to check a panel keeps its key after a simulated server re-render with a higher revision.
Why
revalidatePath inside a Server Action makes the response carry a fresh RSC tree for the current route, so any key derived from render time or from a value the action just changed forces a remount.
Gotchas
- A vi.mock('react') that replaces hooks breaks renderToStaticMarkup in the same file unless each hook delegates to the real one when no test mount is active
- A test hooks engine must defer setState called during render (mark dirty, re-render after) instead of rendering recursively
- ESLint react-hooks flags an internal helper named useMemo called from useCallback in the test engine: name it memo
Code Snippets
Stable key and sync of newer server data
// page.tsx
<Editor key={w.id} workflow={...} />
// editor.tsx
const [seen, setSeen] = useState(workflow.revision);
if (workflow.revision !== seen) {
setSeen(workflow.revision);
if (workflow.revision > revision) { setDef(workflow.definition); setRevision(workflow.revision); }
}Context
Client editors rendered by a dynamic App Router page whose Server Actions call revalidatePath on the same route
Revisions (0)
No revisions yet.