Part of #1946 — Redux → React Query migration (Stage 1).
Goal: all course-tab data fetching on React Query; remove the courseHome reducer.
Two prerequisites were tracked as their own sub-issues: courseware search (#1979) and the CTA toast → ToastProvider (#1980) — the shared TabPage had to be off Redux before the tabs converted.
Done (each its own sub-issue of #1946, stacked in #2062)
Remaining — one layer, closes this issue
- The slice is down to one live field, proctoringPanelStatus: written by ProctoringInfoPanel when its getProctoringInfoData fetch settles, read by OutlineTab (defers the weekly-goal card) and ProductTours (holds the outline tour until the card exists). Four other initial-state fields (courseStatus, courseId, errorMessage, errorCode) are dead.
- Convert getProctoringInfoData to a useProctoringInfoData query; both readers read "settled" off the query's status (same key → one fetch); the panel derives its display state from data instead of six useStates.
- Delete slice.js and course-home/data/index.js; drop courseHome from store.ts and setupTest.js's initializeTestStore.
Verify: courseHome reducer gone (store.ts = models + specialExams + plugins); outline tab still defers the weekly-goal card until the proctoring panel settles; outline-tab tour still waits for it; git grep courseHome src/store.ts src/setupTest.js empty; Pact test green.
Note
The plan below was written by Claude (Claude Code) and reviewed before posting.
Plan for the close-out layer
Why a query, not lifted state
The Redux field is a hand-rolled loading flag for one fetch. The tab conversions already replaced such flags (courseStatus strings) with query state, so the same move applies: make the fetch a query and read isPending off it. Both readers call the hook; React Query dedupes by key; no new context or lifted useState.
Pieces
- course-home/data/queryKeys.ts + apiHooks.ts — proctoringInfo(courseId, username) key; useProctoringInfoData(courseId, username, enabled = true) wrapping the unchanged getProctoringInfoData (its 404 → {} mapping stays in the api).
- ProctoringInfoPanel.jsx — replace the mount effect + six useStates with the query and plain derivation from data (status, link, expiry → readable status, release date, past-due, show-panel = non-empty response). The response is raw snake_case today; keep the reads or camelCase in the hook — same rendered output either way. useDispatch, the slice import, and the exhaustive-deps disable leave.
- OutlineTab.jsx — (!enableProctoredExams || proctoringPanelStatus === 'loaded') → (!enableProctoredExams || !isPending) from the same hook (username already in scope from courseHomeMeta).
- ProductTours.jsx — same hook with enabled: outlineTabActive (the panel only ever mounted on the outline tab, so no new request elsewhere). Username must be the courseHomeMeta one the panel keys on (masquerade-aware), not getAuthenticatedUser().username, or the query key forks under masquerade — LoadedTabPage already reads courseHomeMeta and passes it as a prop (revisit at code review).
- Delete slice.js, course-home/data/index.js; drop courseHome from store.ts and initializeTestStore. Check whether LOADED in constants.ts loses its last importer.
Settled-vs-error (decided: !isPending + retry: false)
The panel's .catch(() => {}) and .finally(resolve) were added together in PR #727 (2021) so the goals widget would never stay hidden because of this request — settling on any outcome is the intent. The catch's "API throws 404" comment was already wrong when written (the api had mapped 404 → {} since the panel's first commits), and before #727 a 5xx surfaced as an unhandled rejection, so silencing real failures was a side effect, not a choice. The query keeps the intent and drops the accident: readers gate on !isPending (never isSuccess); retry: false so a failure settles immediately as #727 designed rather than after the app default's ~7s of 5xx retries; no swallow in queryFn, so the error reaches the app QueryCache.onError and is logged. Rendered output on error is unchanged (no data → panel hidden).
Tests
- apiHooks.test.tsx: payload resolves; 404 → {}; enabled: false idle; non-404 → isError.
- OutlineTab.test.jsx: the existing proctoring block renders the panel against a mocked endpoint and should pass unchanged; add a weekly-goal-deferral case if none asserts the gate today.
- ProductTours.test.jsx: already mocks the proctoring URL (404) so the outline tour proceeds; passes once the hook keys on the same username the mock uses.
Commit
refactor: retire the courseHome Redux slice — Closes #1975. Not breaking: proctoringPanelStatus was never a documented or plugin-facing surface.
After this layer
Stack #2062 layer 16. Then #2017 (plugins reducer) and #1977 (models reducer), leaving store.ts at specialExams only — #1978's endpoint.
Part of #1946 — Redux → React Query migration (Stage 1).
Goal: all course-tab data fetching on React Query; remove the courseHome reducer.
Two prerequisites were tracked as their own sub-issues: courseware search (#1979) and the CTA toast → ToastProvider (#1980) — the shared TabPage had to be off Redux before the tabs converted.
Done (each its own sub-issue of #1946, stacked in #2062)
Remaining — one layer, closes this issue
Verify: courseHome reducer gone (store.ts = models + specialExams + plugins); outline tab still defers the weekly-goal card until the proctoring panel settles; outline-tab tour still waits for it; git grep courseHome src/store.ts src/setupTest.js empty; Pact test green.
Note
The plan below was written by Claude (Claude Code) and reviewed before posting.
Why a query, not lifted state
The Redux field is a hand-rolled loading flag for one fetch. The tab conversions already replaced such flags (courseStatus strings) with query state, so the same move applies: make the fetch a query and read isPending off it. Both readers call the hook; React Query dedupes by key; no new context or lifted useState.
Pieces
Settled-vs-error (decided: !isPending + retry: false)
The panel's .catch(() => {}) and .finally(resolve) were added together in PR #727 (2021) so the goals widget would never stay hidden because of this request — settling on any outcome is the intent. The catch's "API throws 404" comment was already wrong when written (the api had mapped 404 → {} since the panel's first commits), and before #727 a 5xx surfaced as an unhandled rejection, so silencing real failures was a side effect, not a choice. The query keeps the intent and drops the accident: readers gate on !isPending (never isSuccess); retry: false so a failure settles immediately as #727 designed rather than after the app default's ~7s of 5xx retries; no swallow in queryFn, so the error reaches the app QueryCache.onError and is logged. Rendered output on error is unchanged (no data → panel hidden).
Tests
Commit
refactor: retire the courseHome Redux slice — Closes #1975. Not breaking: proctoringPanelStatus was never a documented or plugin-facing surface.
After this layer
Stack #2062 layer 16. Then #2017 (plugins reducer) and #1977 (models reducer), leaving store.ts at specialExams only — #1978's endpoint.