Skip to content

Render hygiene, self-clearing error state, and report-flow hardening - #42

Merged
dqnykamp merged 1 commit into
Doenet:mainfrom
dqnykamp:render-hygiene-error-state
Jul 13, 2026
Merged

Render hygiene, self-clearing error state, and report-flow hardening#42
dqnykamp merged 1 commit into
Doenet:mainfrom
dqnykamp:render-hygiene-error-state

Conversation

@dqnykamp

Copy link
Copy Markdown
Member

Fixes #40. Fixes #41. Implements most of #38 and #34.

Error state (#40)

  • A source error is now derived from the source-analysis useMemo instead of being set into state from inside it — updating source from an invalid to a valid activity on the same mounted component clears the error and renders the activity (new cypress spec covers the repro).
  • Runtime attempt-generation errors (whole-activity and per-item) are captured by the reducer instead of throwing — a throw during React's render-phase reducer run unmounts the viewer via an error boundary. They now render as a banner while the activity (whose state the failed action left untouched) stays mounted: no student work is lost, and "New attempt" offers a retry that self-clears the error on success.

React Compiler (#41)

The set-state-in-render suppression that made the compiler bail on the whole Viewer component is gone, along with all three violations it masked:

  • loadedStateNum became a reducer-owned stateVersion counter (no more setState-in-effect);
  • itemSequence is memoized content-stable — every score report rebuilds the state tree without changing the id sequence, and keeping the previous identity keeps itemIdsToRender/checkRender/the memoized item subtrees stable across reports;
  • the render-body addItemToRender scheduling was replaced by deriving the set of items allowed to render from itemsRendered plus the pagination/visibility rules (the accumulate-in-state version was redundant state).

eslint . passes with zero errors and zero react-hooks suppressions in the render path.

Render hygiene (#38)

The Activity tree components are memoized with stable callbacks; per-item slices of doenetStates / itemAttemptNumbers / answerResponseCountsByItem are extracted at the Activity→SingleDoc boundary (index lookups via a Map), so a score report from one document no longer re-renders every item's subtree. ActivityViewer serializes source for prop-change detection only when its identity changes (was: every render), and flags defaults to a stable identity.

Report-flow hardening (from review)

  • The reducer derives a document's sequence position itself (callers no longer pass itemSequence/doenetStateIdx, which required tracking the current sequence in a ref with a stale window) and ignores reports from documents no longer in the activity — such a stale report (e.g. an in-flight save racing a new attempt) used to throw out of the reducer and unmount every item.
  • The DoenetViewer key now includes the frozen state version: externally loaded state (SPLICE.getState) that doesn't change an item's attempt number previously updated only initialState, which the viewer treats as seed-only — the loaded work was silently ignored.

Behavior note

The derived render schedule drops an in-flight (not-yet-initialized) prefetch item when the user navigates away before it finishes booting; it re-mounts when eligible again. Previously such items stayed scheduled forever. Items that finished rendering are unaffected.

Tests

43 vitest (new: runtime-error capture + self-clearing; stale-report ignored) and 8 cypress component tests (new: error-recovery repro for #40) pass; eslint . and vite build clean. Worth a manual pass in the dev harness (pagination, attempt regeneration, load-state) given the thin automated coverage of the render flow noted in #41.

🤖 Generated with Claude Code

Fixes Doenet#40, Doenet#41; implements most of Doenet#38 and Doenet#34.

Error state (Doenet#40): a source error is derived from the source-analysis
useMemo instead of being set into state from inside it, so updating
`source` from an invalid to a valid activity on the same mounted
component leaves the error screen and renders the activity. Runtime
attempt-generation errors (whole-activity and per-item) are captured by
the reducer instead of throwing — a throw during React's render-phase
reducer run would unmount the viewer via an error boundary — and render
as a banner while the activity (whose state the failed action left
untouched) stays mounted, so no student work is lost and "New attempt"
offers a retry that self-clears the error.

React Compiler (Doenet#41): the `set-state-in-render` suppression that made
the compiler bail on the whole Viewer component is gone, along with the
violations it masked: `loadedStateNum` became a reducer-owned
`stateVersion` counter, `itemSequence` is memoized *content-stable*
(a score report rebuilds the state tree without changing the sequence;
keeping the previous identity keeps everything downstream stable), and
the render-body `addItemToRender` scheduling was replaced by deriving
the set of items allowed to render from `itemsRendered` plus the
pagination/visibility rules. `eslint .` passes with zero errors and
zero react-hooks suppressions in the render path.

Render hygiene (Doenet#38): the Activity tree components are memoized with
stable callbacks; the per-item slices of doenetStates /
itemAttemptNumbers / answerResponseCountsByItem are extracted at the
Activity→SingleDoc boundary (index lookups via a Map); the
ActivityViewer serializes `source` for prop-change detection only when
its identity changes, and `flags` defaults to a stable identity.

Report-flow hardening (from review): the reducer derives a document's
sequence position itself (callers no longer pass itemSequence /
doenetStateIdx, which required tracking the current sequence in a ref)
and ignores reports from documents no longer in the activity — such a
stale report used to throw out of the reducer and unmount every item.
The DoenetViewer key now includes the frozen state version: externally
loaded state (SPLICE.getState) that doesn't change an item's attempt
number previously updated only `initialState`, which the viewer treats
as seed-only, so the loaded work was silently ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant