refactor: convert CoursewareContainer to TypeScript - #2021
Open
brian-smith-tcril wants to merge 1 commit into
Open
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## bsmith/react-query-declass-courseware-container #2021 +/- ##
===================================================================================
+ Coverage 93.51% 93.53% +0.01%
===================================================================================
Files 363 363
Lines 5894 5905 +11
Branches 1365 1403 +38
===================================================================================
+ Hits 5512 5523 +11
+ Misses 367 366 -1
- Partials 15 16 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
marked this pull request as ready for review
August 20, 2026 18:29
brian-smith-tcril
force-pushed
the
bsmith/react-query-courseware-container-typescript
branch
from
August 21, 2026 00:58
28d3943 to
5422cfe
Compare
brian-smith-tcril
force-pushed
the
bsmith/react-query-courseware-container-typescript
branch
from
August 21, 2026 01:59
5422cfe to
fe35054
Compare
Rename src/courseware/CoursewareContainer.jsx → .tsx with the minimum annotations to compile. No behavior change; fast-follow to the de-class (#2008/#2020) so the courseware React Query peels happen in TS-land. - state: RootState on the inline useSelectors - useRef<any> for the two loose refs (mirrored props + memoized guards); reselect infers the redirect helpers + createSelector selectors, so no annotations there - courseStatus asserted `as CourseStatus` at the read (a valid CourseStatus from its source); courseId coerced `?? undefined` only at the TabPage prop, since the effect's ids-mismatch bail needs courseId to stay string | null - dispatch stays plain useDispatch(); no typed-dispatch primitive added — typed thunk dispatch would only serve the Redux being removed Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/react-query-courseware-container-typescript
branch
from
August 21, 2026 14:48
fe35054 to
2459906
Compare
arbrandes
approved these changes
Aug 21, 2026
arbrandes
left a comment
Contributor
There was a problem hiding this comment.
Pre-approved, with an optional nit.
| const sectionViaSequenceId = useSelector(sectionViaSequenceIdSelector); | ||
|
|
||
| const latest = useRef(); | ||
| const latest = useRef<any>(); |
Contributor
There was a problem hiding this comment.
Nit: latest could carry its real shape instead of any. But I figure this might be going away, so totally optional.
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
Convert
CoursewareContainerfrom.jsxto.tsx. The diff is tiny — a file rename plus a handful of annotations, no behavior change — and that's the point: the value isn't in this PR, it's in what it unlocks. With the container in TypeScript, the courseware React Query peels (#2010+) convert the data layer in TS-land, so everyuseQuery/useMutationand the code consuming it is type-checked as it lands — instead of writing the conversions in untyped.jsxand retrofitting types later.Fast-follow to the de-class (#2020); part of the Redux → React Query migration (#1946, Stage 1), stacked above #2020 and below the RQ peels. Split from the de-class so each PR reviews as one idea (structural change vs. type annotations). Closes #2019.
What changed
Only
src/courseware/CoursewareContainer.jsx→.tsx(git tracks it as a rename; the diff is just the annotations):state: RootStateon the inlineuseSelectors (matching theTabPage.tsxprecedent);useRef<any>()on the two loose refs. The redirect helpers andcreateSelectorselectors needed no annotation — reselect infers them.TabPage's already-typed props:courseStatus as CourseStatusat the read, andcourseId ?? undefinedat the prop. Both fall away when the peels move this component to RQ (it'll passcourseStatus={{ metadataQuery, tabDataQuery }}and takecourseIdfromuseParams, like the five course-home tabs already do).The decision log covers the full rationale — the two bridges, the alternatives weighed (incl. changing the slice), and why
store.tsis left untouched.Testing
npm run types(0 errors),npm run lint(clean), andCoursewareContainer.test.jsx(70 passing) all green — the test file stays.jsxand unchanged, resolving to the.tsx. No behavior change to exercise; a manual click-through of the courseware player confirmed it renders as before.Decisions
Full decision log
Decisions — convert CoursewareContainer to TypeScript (#2019)
Working notes for this PR (part of the wider Redux → React Query migration,
#1946). Not checked in — referenced when opening the PR. Fast-follow to the
de-class #2008/#2020, stacked directly above it and below the courseware RQ peels
(#2010+). Closes #2019.
This layer is a pure TypeScript conversion: rename
src/courseware/CoursewareContainer.jsx→.tsxand add the minimum annotationsto compile. No behavior change. It exists so the courseware RQ peels all happen
in TS-land; it's split from the de-class (#2020) so each PR reviews as one idea
(structural change vs. type annotations).
Don't type the doomed Redux stuff (
any/loose)Decision. The Redux-sourced values are about to be deleted/replaced as each
fetch becomes a typed
useQueryin the RQ peels, so type them loosely now andtighten per-peel. In practice this needed very little:
checkResumeRedirect, …) and thecreateSelectorselectors (currentCourseSelector, …) needed noannotation — reselect's
defaultMemoize/createSelectortypes infer theirparameters, so
tscwas already satisfied. Noanyadded there.latest,guards) hold doomed shapes (the mirroredprops and the memoized guard fns), so they're
useRef<any>().Dispatch stays plain
useDispatch()— no typed-dispatch primitiveDecision. Do not add an
AppDispatchtype /useAppDispatchhook (store.tsis untouched). Typed thunk dispatch only ever serves the Redux layer we're deleting:
the RQ-converted code uses
useQuery/useMutationand doesn't dispatch at all, andas each peel converts a fetch its
dispatch(...)calls disappear — so there's nofuture where typed dispatch helps the RQ code. Adding it would be investing type
effort in the doomed Redux stuff, against the migration's whole point.
It's also unnecessary to compile: the thunks live in
thunks.js, sotscsees themloosely and plain
useDispatch()type-checks thedispatch(fetchCourse(id))callswithout complaint. (The #2019 issue's task list included adding the primitive — that
premise doesn't hold, so it's dropped;
store.tskeeps only its existingRootState.)useSelectorstate typed asRootStateDecision. The five inline
useSelector((state: RootState) => state.courseware.…)calls are typed with
RootState(matching theTabPage.tsxprecedent), which isenough for
state.courseware.*to resolve. The courseware slice is still JS, sothose values come back loosely inferred — fine for now.
Two interim bridges to
TabPage's (RQ-era) propsTabPage's prop types are designed for the converted world (CourseStatus = StatusValue | { metadataQuery, tabDataQuery },courseId?: string), while thiscomponent is still a Redux caller. Typing the state surfaced two gaps — handled
differently, because they are different kinds of gap:
courseStatus— assert once at the read. The slice value genuinely is avalid
StatusValue, but becauseslice.jsis JS,RootStateinfers the field asthe wider
string, which won't land inCourseStatuson its own. So it's typedas CourseStatusat theuseSelector(import typefrom../tab-page/TabPage),making it a valid
CourseStatusfrom its source — not at the prop. The value iswhat the type says; the assertion just tells TS what the JS slice can't.
courseId— coerce at theTabPageprop only.courseId={courseId ?? undefined}.The slice uses
string | null(null = no course yet);TabPageusesstring | undefined.This one can't move to the read: the effect's ids-mismatch bail
(
courseId !== (routeCourseId || null)) depends oncourseIdstayingstring | null— coercing it to
undefinedglobally would makeundefined !== nullfire the bailon every unloaded render (a behavior change). So
courseIdstaysstring | nulleverywhere and is coerced (a real null→undefined value transform, not a type
assertion) only for the one consumer that wants
undefined. Behavior-safe:TabPagetreats a falsycourseIdthe same either way.Both are interim. When the courseware peels convert this component's data, it'll
pass
courseStatus={{ metadataQuery, tabDataQuery }}(the object arm ofCourseStatus, no cast — exactly as the five course-home tabs already do) and takecourseIdfromuseParams/useContextId(string | undefined, matching directly),and both bridges drop.
Alternatives considered for the
courseIdnull→undefined bridgeWe deliberately kept the inline
courseId ?? undefinedat theTabPageprop afterweighing the alternatives:
undefinedinstead ofnull. Tempting as "prepping theRQ shape," but rejected: the RQ-era
courseIddoes not come from this slice —the Tear down the courseware Redux slice + replace useContextId #1976 teardown deletes
state.courseware.courseIdand sources it fromuseContextId/useParams(alreadystring | undefined). So changing the slicemakes a soon-deleted field briefly mimic the RQ shape without building toward it —
i.e. investing in Redux we're removing. It would also touch the behavior-critical
bail (below), require editing
sequenceIdfor consistency, and need a repo-wideaudit of ~14 readers of
state.courseware.courseId/.sequenceId. Not worth it tosave one coercion; out of scope for a TS-only PR.
slice.js→.ts). Would remove thecourseStatusassertion too, but same objection — typing effort spent on the slice that's deleted
in Tear down the courseware Redux slice + replace useContextId #1976, and out of scope here.
courseIdat the read (globally), likecourseStatus. Rejected: theeffect's bail normalizes both sides to null-when-absent —
courseId !== (routeCourseId || null)— so it relies oncourseIdbeingstring | null. Coercing it tostring | undefinedmakes the unloaded caseundefined !== null→true, firing the bail when it shouldn't (a behavior change).Unlike
courseStatus(a type assertion of an already-valid value), this is a valuetransform that's only correct for the
TabPageconsumer, so it must stay local tothat consumer.
const tabPageCourseId = courseId ?? undefined). Equivalentto the inline form; chose inline since there's a single consumer and it's one line.
The "why" lives here in the doc rather than as a code comment.
Tests unchanged
Decision.
CoursewareContainer.test.jsxstays.jsxand unchanged — itimports
./CoursewareContainer, which now resolves to the.tsx, and the 70 cases(incl. the two coverage tests added in #2020) pass as-is. No behavior change, so no
test edits.
npm run typesandnpm run lintare clean.Manual testing
Manual testing — convert CoursewareContainer to TypeScript (#2019)
A type-only conversion (
.jsx→.tsx) with no runtime/behavior change, sothere's nothing new to exercise beyond confirming the courseware player still
renders. The real safety net is tooling:
npm run types(0 errors),npm run lint(clean), andCoursewareContainer.test.jsx(70 passing, unchanged).Mark results as you go —
[x]pass,[!]problem (add a note).behaves as before. No functional change expected, none observed.
🤖 Generated with Claude Code