openedx / openedx/frontend-app-learning

Convert the progress-tab exam attempts fetch (`courseHome.examsData`) to a React Query hook

Open
#2,075 2 comments 0 reactions 1 assignee View on GitHub

@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

  • fetchExamAttemptsData thunk → a query (fan-out over the subsection ids, {} per 404 / failed entry, positional order preserved).
  • Export a plugin-facing hook; keep null before the first result to match today's initial state.
  • Delete setExamsData / examsData from the courseHome slice and the thunk; convert the five store-inspection tests in ProgressTab.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
  1. course-home/progress-tab/ProgressTab.jsxProgressTabContent takes sectionScores from the progress query, flattens every subsection's blockKey into sequenceIds, and calls useGetExamsData(courseId, sequenceIds).
  2. course-home/progress-tab/hooks.jsxuseGetExamsData is a useEffect that dispatches fetchExamAttemptsData(courseId, sequenceIds).
  3. course-home/data/thunks.jsfetchExamAttemptsData runs Promise.all over the ids, one getExamsData(courseId, sequenceId) each, keeps response.exam || {}, and on any throw does logError(e) and keeps {}. Then dispatch(setExamsData(results)).
  4. course-home/data/api.jsgetExamsData GETs the LMS edx_proctoring attempt endpoint (or the edx-exams student/exam/attempt endpoint when EXAMS_BASE_URL is set), camelCases the body, and maps a 404 to {}.
  5. course-home/data/slice.jssetExamsData stores the array at state.courseHome.examsData (initial null).

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 in ProgressTab.jsx fails exactly the five tests in the Progress Tab › Exam data fetching integration block of ProgressTab.test.jsx (69 of 74 pass); every assertion in that block counts requests or inspects store.getState().courseHome.examsData directly. Nothing rendered changes.
  • No visible external reader. A GitHub code search over the openedx org for examsData finds only this repo's tests and an unrelated instructor-dashboard file. Operator env.config.jsx files 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.jsx inserting one DIRECT_PLUGIN widget into every progress-page slot, reading useSelector(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 nested progress_certificate_status slot 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.
  • pluginProps are the slot contract, and examsData is not in any of them. The six slots pass enableProgressGraph (course completion) or nothing; the nested certificate slot passes courseId. The exam data was reachable only because plugins render inside the app's Redux Provider. Threading it through pluginProps would 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.