Skip to content

Convert the dates tab to React Query #1984

Description

@brian-smith-tcril

Part of #1975 (course-home tab data → React Query), Phase 3 of the epic #1946.

Summary

Convert the dates tab from Redux thunks + model-store to React Query, and establish the per-tab conversion pattern for the rest of course-home: each tab becomes self-wrapping — it renders <TabPage> itself and owns its data-loading via query hooks (the shape CoursewareContainer already uses) — and its wrapper line is removed from index.jsx. TabContainer is not modified; it keeps serving the unconverted tabs + course-exit and is deleted later (Phase 4, with course-exit).

Dates is the pattern-setter because its existing DatesTab.test.jsx (365 lines) mocks at the axios layer and is already wired with QueryClientProvider + a /course/:courseId/dates router, so it survives the thunk→RQ conversion with its assertions intact and acts as the behavior-parity guard.

Tasks

  • New useCourseHomeMeta, useDatesTabData, useCourseHomeTab query hooks (+ src/course-home/data/queryKeys.ts).
  • DatesTab self-wraps <TabPage>; courseId via useParams; courseStatus derived from the queries.
  • Dates subtree (Timeline, Day, ShiftDatesAlert, UpgradeToShiftDatesAlert) switches courseId from useSelector(state.courseHome) to useParams.
  • ShiftDatesAlert's fetch (thunk) prop → an onReset callback: dates invalidates the RQ query; outline keeps dispatching fetchOutlineTab (one-line call-site change, unchanged behavior).
  • Remove the dates wrapper from index.jsx; delete the now-dead fetchDatesTab thunk + re-export.
  • Transitional model-store bridges for courseHomeMeta and dates so the subtree keeps its useModel(...) reads unchanged (removed in Phase 5 / when TabPage reads meta from RQ).
  • Preserve DatesTab.test.jsx teeth (only drop the <TabContainer> wrapper from its harness); add apiHooks.test cases (status matrix + both bridges); update the one index.test route assertion; keep OutlineTab.test green.

Notes / decisions

  • Model-store stays a transitional read cache. This PR moves fetching to React Query; the courseHomeMeta/dates bridges keep the existing readers working. Removing model-store for course-home is Phase 5.
  • courseStatus derivation matches the thunk today: error → FAILED; fetching → LOADING; metadata !courseAccess.hasAccess → DENIED; else LOADED (getDatesTabData swallows 401/403, so access is authoritative from the metadata call).
  • 403 failure detail: on a rare FAILED, TabPage shows the generic failure message rather than the 403 errorMessage/errorCode (access errors go through DENIED). Accepted for now.

Note

The plan below was generated by Claude (Claude Code) and reviewed before posting.

Claude Plan — dates tab conversion

Approach

One tab per PR. The tab component becomes self-wrapping (renders <TabPage> and owns its data via query hooks); TabContainer is untouched and only the converting tab's wrapper line leaves index.jsx. Stacked on the CTA-toast PR (#1982), which already took TabPage off Redux for toast.

Query hooks (src/course-home/data/apiHooks.ts; keys in a new queryKeys.ts)

export const useCourseHomeMeta = (courseId) => useQuery({
  queryKey: courseHomeQueryKeys.metadata(courseId),
  queryFn: () => getCourseHomeCourseMetadata(courseId, 'outline'),
});

export const useDatesTabData = (courseId) => {              // query + transitional 'dates' bridge
  const query = useQuery({
    queryKey: courseHomeQueryKeys.datesTab(courseId),
    queryFn: () => getDatesTabData(courseId),
  });
  const dispatch = useDispatch();
  useEffect(() => {                                          // BRIDGE: subtree still reads useModel('dates')
    if (query.data) { dispatch(addModel({ modelType: 'dates', model: { id: courseId, ...query.data } })); }
  }, [query.data, courseId]);
  return query;
};

export const useCourseHomeTab = (courseId, tabQuery) => {   // generic: meta bridge + status
  const meta = useCourseHomeMeta(courseId);
  const dispatch = useDispatch();
  useEffect(() => {                                          // BRIDGE: TabPage/LoadedTabPage/subtree read useModel('courseHomeMeta')
    if (meta.data) { dispatch(addModel({ modelType: 'courseHomeMeta', model: { id: courseId, ...meta.data } })); }
  }, [meta.data, courseId]);

  let courseStatus = LOADED;
  if (meta.isError || tabQuery.isError) courseStatus = FAILED;
  else if (meta.isLoading || tabQuery.isLoading) courseStatus = LOADING;
  else if (!meta.data.courseAccess.hasAccess) courseStatus = DENIED;
  return { courseStatus };
};

Self-wrapping DatesTab

const DatesTab = () => {
  const { courseId } = useParams();
  const dates = useDatesTabData(courseId);
  const { courseStatus } = useCourseHomeTab(courseId, dates);
  const queryClient = useQueryClient();
  const { isSelfPaced, org } = useModel('courseHomeMeta', courseId);  // bridged
  const { courseDateBlocks } = useModel('dates', courseId);           // bridged
  return (
    <TabPage activeTabSlug="dates" courseId={courseId} courseStatus={courseStatus} metadataModel="courseHomeMeta">
      {courseId && <OuterExamTimer courseId={courseId} />}
      {isSelfPaced && hasDeadlines && (
        <>
          <ShiftDatesAlert model="dates"
            onReset={() => queryClient.invalidateQueries({ queryKey: courseHomeQueryKeys.datesTab(courseId) })} />
          <SuggestedScheduleHeader />
          <BannerDatesUpgradeSlot courseId={courseId} logUpgradeLinkClick={logUpgradeLinkClick} />
          <UpgradeToShiftDatesAlert logUpgradeLinkClick={logUpgradeLinkClick} model="dates" />
        </>
      )}
      <Timeline />
    </TabPage>
  );
};

Files

  • new src/course-home/data/queryKeys.tscourseHomeQueryKeys (metadata, datesTab).
  • edit src/course-home/data/apiHooks.tsuseCourseHomeMeta, useDatesTabData, useCourseHomeTab.
  • edit src/course-home/dates-tab/DatesTab.jsx — self-wrap; useParams; query hooks; onReset invalidate.
  • edit src/course-home/dates-tab/timeline/Timeline.jsx, timeline/Day.jsxcourseId via useParams.
  • edit src/course-home/suggested-schedule-messaging/ShiftDatesAlert.jsxuseParams; fetch prop → onReset; drop useDispatch.
  • edit src/course-home/suggested-schedule-messaging/UpgradeToShiftDatesAlert.jsxcourseId via useParams.
  • edit src/course-home/outline-tab/OutlineTab.jsxShiftDatesAlert fetchonReset (still dispatches fetchOutlineTab).
  • edit src/index.jsx — dates route → <DatesTab />; drop fetchDatesTab import.
  • edit src/course-home/data/thunks.js + index.js — remove fetchDatesTab + re-export.

Tests

  • DatesTab.test.jsx — drop the <TabContainer> wrapper only; keep all assertions incl. "handles shift due dates click" (invalidate-driven refetch clears the banner; toast via ToastProvider).
  • apiHooks.test.tsxuseDatesTabData/useCourseHomeMeta fetch, useCourseHomeTab status matrix, both bridges (addModel).
  • index.test.jsx — dates route now renders the Dates Tab mock (was Tab Container).
  • OutlineTab.test.jsx — green-check after the ShiftDatesAlert prop swap.

Verification

nvm use && npm run types && npm run lint && npm test (targeted first: DatesTab, apiHooks, OutlineTab, index, TabContainer), then full suite + npm run build. git grep fetchDatesTab → gone. Manual: timeline + suggested-schedule alerts render; "Shift due dates" refreshes the banner + toast; loading/denied/failed via TabPage; other tabs unaffected.

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions