refactor: derive the courseware loaded gate and sequence ids from queries - #2071
Draft
brian-smith-tcril wants to merge 1 commit into
Draft
brian-smith-tcril wants to merge 1 commit into
brian-smith-tcril wants to merge 1 commit into
Conversation
…ries
Adds useIsCourseLoaded — the course bridge's composite collapsed to a
boolean, since every remaining consumer (nav hooks, breadcrumbs, the
teardown's redirect logic) gates on exactly "loaded"; the denied/failed
distinctions live in TabPage, which takes the raw queries — and
useSequenceIds, replacing sequenceIdsSelector with the same gate + flatMap
over the section models (useModels' shallowEqual + useMemo preserve the
memoization createSelector provided). useSequenceNavigationMetadata and
UnitNavigationEffortEstimate convert; courseware/data/selectors.js is
deleted (its other consumers converted in the route-ids layer); the
container's inline copy remains until the teardown.
Also defaults useSequenceNavigationMetadata's entranceExamData destructure
(= {}): it throws on the empty coursewareMeta model, a landmine the
always-set slice courseId kept buried and route-sourced ids reach.
Part of #1976.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
brian-smith-tcril
added this pull request to stack #2062
September 16, 2026 08:01
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## bsmith/use-sequence-status #2071 +/- ##
==============================================================
+ Coverage 93.75% 93.77% +0.02%
==============================================================
Files 368 367 -1
Lines 6033 6039 +6
Branches 1392 1428 +36
==============================================================
+ Hits 5656 5663 +7
+ Misses 361 359 -2
- Partials 16 17 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Move the last scattered courseware status/id readers onto queries:
useIsCourseLoaded(courseId)— the bridge's four-state composite collapsed to a boolean, since every consumer outside TabPage gates on exactly "loaded" — anduseSequenceIds(courseId), replacingsequenceIdsSelectorwith the same gate + flatMap over the section models.useSequenceNavigationMetadataandUnitNavigationEffortEstimateconvert, andcourseware/data/selectors.jsis deleted. This is layer A3 (of six) of the courseware slice teardown #1976 (plan), stacked on the sequence-readers layer #2070. Part of #1976 — the teardown's final layer closes it.After this layer the transitional status bridge's only remaining readers are
CoursewareContainer,CourseBreadcrumbs(next layer), andTabPage.What changed
courseware/data/apiHooks.ts:useIsCourseLoadedreturnsmetadataQuery.isSuccess && courseHomeMetaQuery.isSuccess && hasAccess && outlineQuery.isSuccess— the bridge'sLOADEDarm as a boolean; its loading/denied/failed arms all behaved as "not loaded" for these consumers, exactly as their string comparisons did (the denied/failed distinctions live in TabPage, which takes the raw queries).useSequenceIdsgates on it and flatMapscoursewareMeta.sectionIdsthrough the sections models — model reads stay onuseModel/useModels(read conversions are Dissolve the model-store normalized cache #1977's home), withshallowEqual+useMemokeeping the returned array referentially stable. That's a mild upgrade for these consumers: the deletedselectors.jsversion was a plain unmemoized function, souseSelectorminted a fresh array on every store dispatch.useSequenceNavigationMetadata(courseIdfromuseParams, gate =isCourseLoaded && sequenceQuery.isSuccess) andUnitNavigationEffortEstimate.react-reduxleaves both files.useSequenceNavigationMetadata'sentranceExamDatadestructure gains a= {}default — it throws on the emptycoursewareMetamodel, a pre-existing landmine that the always-set slicecourseIdkept buried and route-sourced ids reach (it error-paged four test suites). One default on the hook's own read makes it total; unlike the refactor: convert outline tab from Redux to React Query #1997 defaults-vs-deriveView case there's no upstream invariant to restore — this hook runs during loading renders by design.courseware/data/selectors.js(itsgetSequenceId/getSequenceStatusconsumers converted in refactor: read courseware route ids from useParams, not the Redux slice #2069) + thecourseware/data/index.jsexport. The container's separate inlinesequenceIdsSelectorcopy remains until the teardown layer.useSequenceIds— the real consumer — instead of a dispatched slice status; a newuseIsCourseLoadedmatrix ports the course half of the status-bridge tests as booleans (per-case axios-mock overrides register before the happy-path mocks — first handler wins);UnitNavigation.testgets the established recipe (fresh store per test, real route with a splat, loaded-state waits anchored on signals that distinguish loaded,fireEvent→userEvent).statusBridge.teststays green alongside until the bridge dies.Testing
npm run types(0 errors),npm run lint(clean), full jest suite green at head (111 suites, 1131 passed / 3 pre-existing skips). Manual pass on tutor local in the details block below.Decisions
Full decision log
Decisions — useIsCourseLoaded + useSequenceIds (#1976, layer A3)
The course composite collapses to one boolean:
useIsCourseLoaded.The bridge derived a four-state string (
loading/loaded/denied/failed) from three queries; every consumer this layer and later touches(the sequence-navigation hooks, the effort estimate, A4's breadcrumbs, B's
redirect logic) gates on exactly "loaded". The denied/failed distinctions
live only in TabPage, which takes the raw queries (B-prep/B). So the hook
returns
metadataQuery.isSuccess && courseHomeMetaQuery.isSuccess && hasAccess && outlineQuery.isSuccess— the bridge'sLOADEDarm as aboolean; its
loading/denied/failedarms all behaved as "not loaded"for these consumers, exactly as their string comparisons did. No status
strings (the A2 decision, carried through).
useSequenceIds(courseId)replacessequenceIdsSelector. Samederivation: gate on loaded, then flatMap the course's
sectionIdsthroughthe sections models (the gate is expressed as data —
useModels('sections', isCourseLoaded ? sectionIds : [])— since hookscan't early-return). Model reads stay on
useModel/useModels(query-result read conversions are Dissolve the model-store normalized cache #1977's home). Referential stability:
useModels'shallowEqualkeeps the sections array reference stable anduseMemokeeps the flatMap result stable. That's a mild upgrade forthese consumers, not just parity — the deleted
selectors.jsversion wasa plain unmemoized function, so
useSelectorre-ran it and produced afresh array on every store dispatch; only the container's separate inline
createSelectorcopy (which remains until B) ever had memoization.courseware/data/selectors.jsis deleted (itsgetSequenceId/getSequenceStatusconsumers converted in A1) andcourseware/data/index.jsdrops the export.Readers converted:
useSequenceNavigationMetadata(
sequence-navigation/hooks.js) takescourseIdfromuseParams,sequenceIdsfrom the new hook, and gates onisCourseLoaded && sequenceQuery.isSuccess(the old'loaded' && 'loaded');UnitNavigationEffortEstimatetakesuseSequenceIds(useParams().courseId).react-reduxleaves both files.useSequenceNavigationMetadata'sentranceExamDatadestructure gains a= {}default.const { entranceExamData: { entranceExamPassed } } = useModel('coursewareMeta', courseId)throws when the model is absent —a pre-existing landmine that the slice-sourced
courseId(always set) andseeded test stores kept buried. With route-sourced ids, any route-less
render reaches it (it error-paged four test suites). One default on the
hook's own read makes it total. This is not the refactor: convert outline tab from Redux to React Query #1997 whack-a-mole
situation (there, the same empty-model destructure was duplicated across
~8 readers and the fix belonged upstream in
deriveView); here it's asingle reader in the file already being edited, and the upstream "both
loaded before render" invariant doesn't exist for this hook — it runs on
every render of the nav components, loading included, by design.
Tests.
coursewareMeta bridgeordering test ("sectionIds survive whenmetadata resolves after the outline") reworks around
useSequenceIdsasthe reader — the real consumer now pins the survival property; its
fetchCourseSuccessdispatch and thesequenceIdsSelectorassertion diewith the selector. It gains the courseHomeMeta mock the loaded gate
needs, and asserts
[]while metadata is still pending.useIsCourseLoadedmatrix — the course half of the bridge testmatrix collapsed to booleans: pending → false, all-success + access →
true, no access → false, outline failure → false, query failure → false,
missing id → false without fetching. Gotcha encoded in the helpers:
axios-mock-adapter matches handlers in registration order (first wins),
so per-case overrides register before the happy-path mocks.
UnitNavigation.testgets the A2 recipe: fresh store per test(
beforeEach— the shared-adapter clobber), arenderNavhelper on/course/:courseId/:sequenceId/*(links navigate mid-test), andloaded-state settles that wait on a signal that actually distinguishes
loaded (an
hrefappearing, or the disabled button materializing)rather than on elements that render identically pre-load.
statusBridge.teststays green alongside until B.Behavior deltas: the standard posture — the nav components' query
subscriptions can trigger background refetches where the bridge was the
sole subscriber; results land in the model store through the same bridge.
Otherwise faithful: same gates, same id ordering; referential stability
improves for the two converted consumers (see 2), which is strictly less
re-rendering, not a behavior change.
Manual testing
Manual testing — useIsCourseLoaded + useSequenceIds (#1976, layer A3)
In-browser verification for layer A3, against tutor local
(
http://apps.local.openedx.io:2000/learning, DemoXcourse-v1:OpenedX+DemoX+DemoCourse). This layer claims zero user-facingchange:
useSequenceNavigationMetadataandUnitNavigationEffortEstimatederive the course-loaded gate and the ordered sequence-id list from queries
instead of the Redux mirrors. The things to watch are unit navigation's
cross-sequence links and first/last-unit states — everything that depends on
the sequence ordering and the loaded gate.
Verify by hand
useSequenceIdsordering) — on thelast unit of a sequence that has a following sequence, Next links to the
next sequence's
/first; on the first unit of a non-first sequence,Previous links to the previous sequence's
/last; both navigate correctly.(Bottom unit navigation is always rendered; no
env.config.jsxneeded.)isFirstUnit/isLastUnitoff the loadedgate) — first unit of the course: Previous disabled; last unit of the
course: Next shows its end-of-course state (disabled, or the
end-of-course/complete-course link per cert state).
keeps working; no flash of wrongly-disabled buttons after load settles.
Results
All three checks passed (2026-09-16, tutor local, DemoX, run against the local
branch @
05011cba, before any push): cross-sequence Next/Previous land onthe following sequence's
/firstand the preceding sequence's/last, thecourse's first unit disables Previous and its last unit shows the correct
end-of-course Next state, and mid-sequence prev/next work with no flash of
wrongly-disabled buttons after load.
🤖 Generated with Claude Code