openedx / openedx/frontend-app-learning
Convert the progress-tab exam attempts fetch (`courseHome.examsData`) to a React Query hook
@brian-smith-tcril is already working on this.
Since Sep 17, 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). Part of the courseHome slice teardown (#1975); split out because it is a plugin-facing contract change, not just a slice removal.
What it is. PR #1829 made the progress tab fetch exam attempt status for every subsection and store the array at state.courseHome.examsData, so that plugins in the progress-tab slots could read it. Nothing in this repo reads it; 2U's fork carries it (edx/frontend-app-learning#38). The fetch uses the same endpoints as @edx/frontend-lib-special-exams' own fetchExamAttemptsData, but fans out course-wide instead of one sequence at a time. It is learning's own Redux, so it is not gated on #2018.
Goal. Keep the data available to plugins without Redux: an exported React Query hook (imported from learning's source, the way operator env.config.jsx files already import components) replaces the useSelector read. pluginProps are the slot contract and the exam data is deliberately not added to them.
Tasks
fetchExamAttemptsDatathunk → a query (fan-out over the subsection ids,{}per 404 / failed entry, positional order preserved).- Export a plugin-facing hook; keep
nullbefore the first result to match today's initial state. - Delete
setExamsData/examsDatafrom thecourseHomeslice and the thunk; convert the five store-inspection tests inProgressTab.test.jsx. - Document the new import for plugin authors, and mark the PR breaking (
refactor!:+BREAKING CHANGE:), naming the old store path and the new hook.
Verify. With an env.config.jsx that inserts a widget into each progress-tab slot and renders the hook's result: same array in every slot as the Redux baseline; git grep examsData src finds only the new hook and its tests; courseHome slice has no examsData.
[!NOTE]
This issue was authored by Claude (Claude Code) and reviewed before posting. The findings it rests on are below.
Findings
How it works today
course-home/progress-tab/ProgressTab.jsx—ProgressTabContenttakessectionScoresfrom the progress query, flattens every subsection'sblockKeyintosequenceIds, and callsuseGetExamsData(courseId, sequenceIds).course-home/progress-tab/hooks.jsx—useGetExamsDatais auseEffectthat dispatchesfetchExamAttemptsData(courseId, sequenceIds).course-home/data/thunks.js—fetchExamAttemptsDatarunsPromise.allover the ids, onegetExamsData(courseId, sequenceId)each, keepsresponse.exam || {}, and on any throw doeslogError(e)and keeps{}. Thendispatch(setExamsData(results)).course-home/data/api.js—getExamsDataGETs the LMSedx_proctoringattempt endpoint (or the edx-examsstudent/exam/attemptendpoint whenEXAMS_BASE_URLis set), camelCases the body, and maps a 404 to{}.course-home/data/slice.js—setExamsDatastores the array atstate.courseHome.examsData(initialnull).
So the value a plugin reads is an array, positional with sectionScores's flattened subsection order, one entry per subsection, {} for subsections with no exam (or any fetch failure), otherwise the camelCased exam object; null until the first fetch resolves.
getExamsData hits the same two URLs as @edx/frontend-lib-special-exams' fetchExamAttemptsData (dist/data/api.js, v4.1.0). The library fetches one exam — the sequence rendered by SequenceExamWrapper — into its own specialExams slice; the progress tab needs attempt status for every subsection at once on a page that renders no exam, so #1829 re-implemented the fetch and fanned it out. If #2018 ends in a React Query conversion of the library, a follow-on could have the library export the query hook and the progress tab drop its copy.
What we established
- No in-repo reader. Deleting only the
useGetExamsData(...)call inProgressTab.jsxfails exactly the five tests in theProgress Tab › Exam data fetching integrationblock ofProgressTab.test.jsx(69 of 74 pass); every assertion in that block counts requests or inspectsstore.getState().courseHome.examsDatadirectly. Nothing rendered changes. - No visible external reader. A GitHub code search over the
openedxorg forexamsDatafinds only this repo's tests and an unrelated instructor-dashboard file. Operatorenv.config.jsxfiles and fork-private plugins are not searchable, so absence there proves nothing — the fork cherry-pick is the evidence of use. - Baseline probe. A local
env.config.jsxinserting oneDIRECT_PLUGINwidget into every progress-page slot, readinguseSelector(state => state.courseHome.examsData), shows the identical array in all six slots that mount on the page (progress_tab_course_completion,progress_tab_certificate_status_main_body,progress_tab_course_grade,progress_tab_grade_breakdown,progress_tab_certificate_status_side_panel,progress_tab_related_links). The nestedprogress_certificate_statusslot only mounts when a certificate card renders. Against the Tutor demo course every entry is{}(no proctored exams → every fetch is a 404); with a dev-only stub returning one exam per subsection, all six slots show the same 17-entry array. pluginPropsare the slot contract, andexamsDatais not in any of them. The six slots passenableProgressGraph(course completion) or nothing; the nested certificate slot passescourseId. The exam data was reachable only because plugins render inside the app's ReduxProvider. Threading it throughpluginPropswould add it to six slots' contracts.
Decision
Replace the Redux path with an exported React Query hook that a plugin imports from learning's source. React Query dedupes by key, so the plugin's call and the progress tab's call share one fetch and no context provider is needed — the query cache does the job the Redux store did. The hook preserves the value shape a plugin sees today. The Redux read stops working, so the PR is marked breaking as #2016 was for the sidebar prefetch contract.
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.