openedx / openedx/frontend-app-learning
Convert course-home tab data to React Query
@brian-smith-tcril is already working on this.
Since Aug 5, 2026.
- Dominant language
- JavaScript
- Stars
- 70
- Forks
- 335
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 35
Description
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)
- The five tabs: dates #1984, outline #1991 (incl.
dismissWelcomeMessage), progress #1998, live #2002, discussion #2003 — thefetchTabmachinery andTabContainer.jsxorchestration are gone. requestCert→ mutation (#1994).fetchExamAttemptsData→useExamAttemptsData+ the plugin-facinguseExamsData()(#2075 → PR #2077); split out becausecourseHome.examsDatawas a plugin-facing read (PR #1829), so its removal is a breaking change on its own.course-home/data/thunks.js,redux.test.js,slice.test.jsdeleted along the way;api.test.jsand the Pact test (course-home/data/pact-tests/lmsPact.test.jsx) stay — they testapi.js, which is unchanged.
Remaining — one layer, closes this issue
- The slice is down to one live field,
proctoringPanelStatus: written byProctoringInfoPanelwhen itsgetProctoringInfoDatafetch settles, read byOutlineTab(defers the weekly-goal card) andProductTours(holds the outline tour until the card exists). Four other initial-state fields (courseStatus,courseId,errorMessage,errorCode) are dead. - Convert
getProctoringInfoDatato auseProctoringInfoDataquery; both readers read "settled" off the query's status (same key → one fetch); the panel derives its display state fromdatainstead of sixuseStates. - Delete
slice.jsandcourse-home/data/index.js; dropcourseHomefromstore.tsandsetupTest.js'sinitializeTestStore.
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 unchangedgetProctoringInfoData(its 404 →{}mapping stays in the api).ProctoringInfoPanel.jsx— replace the mount effect + sixuseStates with the query and plain derivation fromdata(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 theexhaustive-depsdisable leave.OutlineTab.jsx—(!enableProctoredExams || proctoringPanelStatus === 'loaded')→(!enableProctoredExams || !isPending)from the same hook (username already in scope fromcourseHomeMeta).ProductTours.jsx— same hook withenabled: outlineTabActive(the panel only ever mounted on the outline tab, so no new request elsewhere). Username must be thecourseHomeMetaone the panel keys on (masquerade-aware), notgetAuthenticatedUser().username, or the query key forks under masquerade —LoadedTabPagealready readscourseHomeMetaand passes it as a prop (revisit at code review).- Delete
slice.js,course-home/data/index.js; dropcourseHomefromstore.tsandinitializeTestStore. Check whetherLOADEDinconstants.tsloses 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: falseidle; 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.