Skip to content

Convert sequence data to React Query #2011

Description

@brian-smith-tcril

Part of #1946 — Redux → React Query migration (Stage 1). Part of the #1976 courseware decomposition (plan) — Target 1 (courseware player). Stacked on the metadata conversion (#2023).

Goal: convert the sequence fetch to React Query.

Tasks

  • Convert fetchSequence → query hook feeding the sequences + units models via the extended bridge.
  • Preserve the 422 sequenceMightBeUnit path (CoursewareContainer relies on the failed request to detect a unit).
  • Keep the courseware slice sequenceId/sequenceStatus written transitionally.

Verify: sequence + units render from the query; unit-vs-sequence (422) detection still works; useModel('units')/useModel('sequences') readers unaffected.

Full implementation plan (drafted by Claude 🤖)

Context

fetchSequence is the last big courseware fetch thunk still on Redux: it loads one sequence's metadata + its units, mirrors them into the sequences/units model store, and drives the courseware slice's sequenceId/sequenceStatus/sequenceMightBeUnit fields.

The conversion is a faithful continuation of the pattern #2023 established for metadata/outline:

  • an RQ query hook whose meta.models mirrors the result into the model store via the bridgeToModelStore QueryCache onSuccess (src/data/modelStoreBridge.ts), and
  • a small transitional status-bridge hook (src/courseware/data/statusBridge.ts) that writes the still-Redux slice status fields, so the many not-yet-converted readers (the container's redirect helpers, Sequence.jsx, breadcrumbs, sequence-navigation, sequence-alerts, the outline sidebar) keep working unchanged.

The one subtlety unique to sequences: CoursewareContainer relies on the sequence fetch failing (a backend 422) to detect that a URL segment is actually a unit, not a sequence (sequenceMightBeUnit) — and on a fast failure, because the redirect that resolves the unit's parent sequence only fires once sequenceStatus === 'failed'.

Key files

  • src/courseware/data/thunks.jsfetchSequence: the thunk being converted.
  • src/courseware/data/apiHooks.ts — where useCoursewareMetadata/useCoursewareOutline live; add useSequenceMetadata here.
  • src/courseware/data/statusBridge.tsuseCourseStatusBridge pattern; add useSequenceStatusBridge here.
  • src/courseware/data/queryKeys.tscoursewareQueryKeys; add a sequence key.
  • src/courseware/data/api.jsgetSequenceMetadata(sequenceId, params) (returns { sequence, units } via normalizeSequenceMetadata).
  • src/courseware/data/slice.jsfetchSequenceRequest/Success/Failure reducers (kept; now bridge-driven).
  • src/courseware/CoursewareContainer.tsx — the sole production caller (the checkFetchSequence guard).
  • src/data/modelStoreBridge.ts — supports updateModel + updateModels strategies with source.
  • src/queryClient.tsonError maps meta.logStatusAs[status]logInfo/logError.
  • src/setupTest.jsseedCoursewareModels (extend) and the executeThunk(fetchSequence(...)) seed loop (remove).
  • src/courseware/data/redux.test.js — the fetchSequence describe block + the "Thunks that require fetched sequences" beforeEach to reseat.
  • src/courseware/data/index.jsfetchSequence re-export (remove).

The conversion

1. queryKeys.ts — add a sequence key

sequence: (sequenceId, isPreview) => [...coursewareQueryKeys.all, 'sequence', sequenceId, isPreview] as const
(preview flips the preview=1/0 request param, so it must be part of the key).

2. apiHooks.tsuseSequenceMetadata(sequenceId, isPreview)

export const useSequenceMetadata = (sequenceId: string | undefined, isPreview: boolean) => useQuery({
  queryKey: coursewareQueryKeys.sequence(sequenceId!, isPreview),
  queryFn: async () => {
    const { sequence, units } = await getSequenceMetadata(sequenceId, { preview: isPreview ? '1' : '0' });
    if (sequence.blockType !== 'sequential') {
      // Non-sequential (e.g. 'chapter') block: fail so the bridge doesn't mirror it and
      // sequenceStatus goes 'failed' (matches the old fetchSequenceFailure branch).
      throw new Error(
        `Requested sequence '${sequenceId}' has block type '${sequence.blockType}'; expected block type 'sequential'.`,
      );
    }
    return { sequence, units };
  },
  enabled: !!sequenceId,
  retry: false,
  meta: {
    logStatusAs: { 422: 'info' },
    models: [
      { modelType: 'sequences', strategy: 'updateModel', source: 'sequence' },
      { modelType: 'units', strategy: 'updateModels', source: 'units' },
    ],
  },
});
  • retry: false — the old thunk never retried, and this keeps the expected 422 (unit detection) failing fast instead of RQ's default 3× exponential backoff (~7s) stalling the redirect. The only behavior that would visibly regress without it.
  • logStatusAs: { 422: 'info' } — the 422 is the expected sequenceMightBeUnit signal; log it at info level like the outline's expected 403. (Behavior note: the old thunk logged nothing on 422; the onError bridge only offers error/info, and a truly silent level would be new machinery — info matches the established 403 treatment.)
  • meta.models — exactly the two dispatches the thunk did (updateModel sequence + updateModels units), both merges, keyed by id from the normalized payload.
  • Throwing on non-sequential (instead of logError + failure dispatch) lets the single onError path log it once; message content is preserved.

3. statusBridge.tsuseSequenceStatusBridge(sequenceId, isPreview)

Mirror of useCourseStatusBridge:

export const useSequenceStatusBridge = (sequenceId: string | undefined, isPreview: boolean) => {
  const dispatch = useDispatch();
  const query = useSequenceMetadata(sequenceId, isPreview);

  useEffect(() => {
    if (!sequenceId) { return; }               // matches the old `if (id)` guard
    if (query.isPending) { dispatch(fetchSequenceRequest({ sequenceId })); return; }
    if (query.isSuccess) { dispatch(fetchSequenceSuccess({ sequenceId })); return; }
    const sequenceMightBeUnit = query.error?.response?.status === 422;
    dispatch(fetchSequenceFailure({ sequenceId, sequenceMightBeUnit }));
  }, [sequenceId, query, dispatch]);
};

The if (!sequenceId) return; guard is essential: a disabled query reports isPending === true, so without it the course-root case (no sequenceId) would wrongly dispatch fetchSequenceRequest. Uses getResponseStatus (src/data/http-error.ts) for the status read, consistent with the rest of the RQ code.

4. CoursewareContainer.tsx

  • Remove fetchSequence from the ./data import and delete the checkFetchSequence guard and its call.
  • Add useSequenceStatusBridge(routeSequenceId, isPreview) next to useCourseStatusBridge(routeCourseId).
  • Everything else is untouched: the redirect helpers and selectors still read sequenceStatus/sequenceMightBeUnit/sequences/units from the slice/model store, which the bridge keeps writing. checkSaveSequencePosition still dispatches the saveSequencePosition thunk (that's Convert saveIntegritySignature + saveSequencePosition to React Query mutations #2015, out of scope).

Cleanup (full)

5. Delete the fetchSequence thunk

  • Remove fetchSequence from thunks.js and from index.js.
  • Keep the fetchSequenceRequest/Success/Failure slice actions — now dispatched by the bridge; Sequence.test.jsx also dispatches fetchSequenceFailure directly.

6. setupTest.js — migrate the sequence seed

  • Extend seedCoursewareModels(store, courseId) to also seed sequences/units directly (fetch each sequence block's metadata via getSequenceMetadata against the existing axios mocks and dispatch updateModel/updateModels, the same shape the bridge produces), plus set the slice status if any existing test depends on sequenceStatus === 'loaded' without rendering the container.
  • Remove the executeThunk(fetchSequence(block.id)) seed loop and the now-unused fetchSequence import. Preserve the excludeFetchSequence option's effect.

7. redux.test.js

  • Remove the Test fetchSequence describe block (networkError / non-sequential / normalize+mirror).
  • Reseat the "Thunks that require fetched sequences" beforeEach (which currently executeThunk(fetchSequence(...))) onto the new direct seed so checkBlockCompletion / saveSequencePosition tests still have populated sequence/unit models.

Tests (new coverage for the converted path)

Add a test that renders useSequenceMetadata + useSequenceStatusBridge through createTestQueryClient(store) (the bridged client) — extending src/courseware/data/apiHooks.test.tsx or a sibling statusBridge-oriented test — migrating the three old thunk cases into hook/bridge terms:

  • success: sequence mirrored into models.sequences (merge) and units into models.units; slice → sequenceStatus: 'loaded', sequenceId set.
  • 422: query errors, no models mirrored; slice → sequenceStatus: 'failed', sequenceMightBeUnit: true; and the query fn is called once (asserts retry: false).
  • non-sequential blockType: query errors (thrown), slice → sequenceStatus: 'failed', sequenceMightBeUnit: false, and logError called once.
  • network error: slice → sequenceStatus: 'failed', logError called.

CoursewareContainer.test.jsx (70 tests, incl. the 422 unit-detection case and the marker/section/unit redirect matrix) should stay green untouched — it drives the container by URL + axios mocks through the bridged query client, so the behavior it asserts is preserved. Adjust only if the de-thunking forces a mechanical change.

Verification

  • npm run test -- src/courseware/CoursewareContainer.test.jsx src/courseware/data — container matrix + new hook/bridge tests + reseated redux.test green.
  • npm run types and npm run lint.
  • Manual smoke (tutor local, DemoX): unit ↔ sequence nav, the bare-unit and section+unit redirect chains (422 unit detection resolves promptly, no ~7s stall), first/last markers, saved unit position, and no console error for the expected 422 (should be an info page action).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions