refactor: convert course-home CTA toast from Redux to a React ToastProvider - #1982
Open
brian-smith-tcril wants to merge 1 commit into
Open
refactor: convert course-home CTA toast from Redux to a React ToastProvider#1982brian-smith-tcril wants to merge 1 commit into
brian-smith-tcril wants to merge 1 commit into
Conversation
brian-smith-tcril
marked this pull request as ready for review
August 7, 2026 06:15
brian-smith-tcril
force-pushed
the
bsmith/react-query-course-home-toast
branch
from
August 7, 2026 06:16
2a92c61 to
76d2c03
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1982 +/- ##
==========================================
+ Coverage 92.61% 92.84% +0.23%
==========================================
Files 358 360 +2
Lines 5851 5874 +23
Branches 1368 1406 +38
==========================================
+ Hits 5419 5454 +35
+ Misses 413 402 -11
+ Partials 19 18 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
brian-smith-tcril
force-pushed
the
bsmith/react-query-course-home-toast
branch
from
August 7, 2026 06:29
76d2c03 to
bc03f2a
Compare
brian-smith-tcril
force-pushed
the
bsmith/react-query-course-home-toast
branch
2 times, most recently
from
August 7, 2026 07:18
499f731 to
051cf57
Compare
brian-smith-tcril
force-pushed
the
bsmith/react-query-course-home-toast
branch
from
August 7, 2026 15:27
051cf57 to
3817200
Compare
7 tasks
brian-smith-tcril
force-pushed
the
bsmith/react-query-course-home-toast
branch
from
August 7, 2026 21:26
3817200 to
7cb895a
Compare
…ovider The shared TabPage read the call-to-action toast from the courseHome Redux slice, so no course-home tab could be de-Redux'd while the toast stayed in Redux. This extracts it end to end (part of #1946, Phase 3): - Toast client state -> a new React ToastProvider/useToast context (src/generic/ToastContext.tsx), mounted at the route root. Message and visibility are decoupled (setToastContent vs openToast/closeToast), removing the slice's show={!!toastHeader} content/visibility fusion. TabPage renders the single <Toast> from the context. - The two POST writers that feed it -> React Query mutations in src/course-home/data/apiHooks.ts (useResetDeadlines, usePostEvent), following the product-tours mutation pattern. The hooks own only the POST + toast and carry no Redux; the transitional model-store refresh stays in each caller's mutate onSuccess (dispatch(getTabData)) until that data is RQ, keeping apiHooks.ts Redux-free. - Deletes the resetDeadlines/processEvent thunks (and the index.js re-export) and the setCallToActionToast reducer + toast fields from the slice. The processEvent postMessage parse/guard moves into useIFrameBehavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
brian-smith-tcril
force-pushed
the
bsmith/react-query-course-home-toast
branch
from
August 7, 2026 22:15
7cb895a to
e71919b
Compare
This was referenced Aug 7, 2026
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
Extracts the course-home call-to-action toast from Redux to a React context, and converts the two POSTs that feed it to React Query mutations. Part of the Redux → React Query migration (#1946), Phase 3 (course-home), stacked on the courseware-search conversion (#1970, the branch below). This is the prerequisite that lets the course-home tabs convert without a Redux toast bridge.
Behavior is preserved — same toast content, action link, auto-hide, and manual close, plus the same post-action data refresh — verified with the full test suite and a live manual smoke pass on both
masterand this branch (with a small fade-out improvement noted below).What changed
ToastProvider/useToast(src/generic/ToastContext.tsx), mounted once at the route root;TabPagerenders the single Paragon<Toast>from it. Message and visibility are decoupled (setToastContentvsopenToast/closeToast), removing the slice'sshow={!!toastHeader}content-vs-visibility fusion.useResetDeadlines/usePostEventinsrc/course-home/data/apiHooks.ts, following theproduct-toursmutation recipe. The hooks own only the POST + toast and are Redux-free; the transitional model-store refresh (dispatch(getTabData)) stays in each caller'smutateonSuccessuntil that data becomes an RQ query.resetDeadlines+processEventthunks (and theresetDeadlinesre-export), and thesetCallToActionToastreducer +toastHeader/toastBodyText/toastBodyLinkfields from the course-home slice.processEventpostMessage parse/guard moves intouseIFrameBehavior(handlePostMessageEvent), sowindow.onmessageis a bare reference;eventTypesis exported fromthunks.js.Testing
Automated:
npm run types,npm run lint,npm run build, and the fullnpm testsuite (106 suites, 896 passing) pass. New unit tests:ToastContext.test.tsx(incl. the decoupling) andcourse-home/data/apiHooks.test.tsx(axios-level POST → toast mapping + bothonErrorpaths). Own-wrapper suites that renderTabPage(DatesTab,DiscussionTab,CoursewareContainer) get the provider.Manual smoke (dev, self-paced course with a missed deadline): exercised both writers —
resetDeadlinesvia the dates-tab "Shift due dates" banner (master + this branch) andusePostEventvia the LMS-rendered in-unitpost_eventbanner (this branch). Toast content, auto-hide, and the post-action refresh match master; the fade-out is cleaner here (content persists through the animation instead of blanking).Decisions
Full decision log
Decisions — Redux → React Query: course-home CTA toast
Working notes for this PR (part of the wider Redux → React Query migration,
#1946). Not checked in — referenced when opening the PR. First PR of Phase 3
(course-home), stacked on the courseware-search conversion (#1970).
Scope & why the toast goes first
The course-home "call-to-action" toast is shared client state. It lives in
the
courseHomeslice (toastHeader/toastBodyText/toastBodyLink+setCallToActionToast), is read/rendered by the sharedTabPage, and iswritten by two POSTs:
resetDeadlinesthunk → fromShiftDatesAlert(dates + outline tabs)processEventthunk → fromuseIFrameBehavior(the courseware in-unit iframe)Because
TabPageis shared by every tab (and courseware), no course-home tab canbe considered de-Redux'd while the toast still reaches Redux. So the toast is
extracted first; the tabs then convert with no toast bridge.
Two moves in this PR: (1) the toast client state → a React
ToastProvidercontext; (2) the two POST writers that feed it → React Query mutations whose
onSuccesssets the toast.Verified: exactly one toast in the whole app — the single Paragon
<Toast>inTabPage.jsx, backed only by those slice members. So this is a scoped extraction,not a general toast framework.
ToastProvider: state only,TabPagerendersNew
src/generic/ToastContext.tsx, mirroringCoursewareSearchContext(useStateuseMemo, hook throws outside its provider). The provider owns only state;TabPagekeeps rendering the single<Toast>, now reading fromuseToast()instead of
useSelector. State in the provider, render in the consumer — the samesplit as the courseware-search context. (We explicitly did not move the
<Toast>component into the provider.)Named
ToastProvider/useToast, notCTAToast…: nothing in the mechanism isCTA-specific — it holds content and renders whatever it's handed. This is not
pre-building for hypothetical future toasts (there's only one today); it's just
naming the thing for what it is rather than for its single current caller.
Consumers call
useToast()directly and the client state lives in the context —no model-store hydration and no prop-drilling-with-fallback bridges. That's the
same shape as the courseware-search and product-tours conversions; the earlier
"hydrate the model store" / "lift to props" options were discarded as not matching
the established pattern.
Placement: mounted once at the route root in
src/index.jsx, wrapping<Routes>— the only common ancestor of bothTabPageparents (TabContainerand
CoursewareContainer, which rendersTabPagedirectly).Message and visibility are fully decoupled (the core design call)
The Redux slice fused content and visibility:
show={!!toastHeader}derivedvisibility from content presence, and closing meant clearing all three content
fields. That fusion is the wart we're removing — not relocating into the
provider. The context exposes two independent pieces of state:
toastContent: ToastContent | null— the current notification contentisToastOpen: boolean— whether the<Toast>is showingNothing couples them. Setting the content does not open the toast, and closing
does not clear the content. A writer that wants to surface a notification does
two explicit things:
setToastContent(...)thenopenToast(). This was adeliberate rejection of a
showToast(content)helper that sets both — the "setalso opens" magic is exactly the coupling we're trying to leave behind. If that
means a mutation's
onSuccesscalls two functions, that's fine.Closing only flips
isToastOpen; the content stays in place, so Paragon'sauto-hide / close-button
onClose(both wired tocloseToast) fades the toast outwith its content rather than blanking it mid-animation.
Context value:
{ toastContent, setToastContent, isToastOpen, openToast, closeToast }.Why the context carries content, not just a boolean
An early question was whether the context could hold only "should the toast show?"
and let
TabPagesupply the text. It can't: the toast text is server-sourced —the POST response's
{ header, link, link_text }is what's displayed, and thereis no client i18n copy for it to hardcode (unlike the dates banner, which does
have its own messages). So whatever emits the toast has to hand the content along;
the provider stores a
ToastContent, not a bare flag, and the writer POSTs feedsetToastContentwith the response.Naming
ToastContent— notToastMessage(it carries amessageand anaction, so "Message" undersells it), notToastData("Data" says nothing).Paragon's
<Toast>takeschildren+action; together those are the toast'scontent.
toastContent/setToastContent— kept aligned with the typename.
isToastOpen,openToast,closeToast—toast-prefixed because they'redestructured from a general hook; a bare
isOpen/showreads ambiguously at thecall site. Mirrors
CoursewareSearchContext'sshow/open/close, extendedwith the content payload.
ToastContentshape (derived from the real APIs, not invented)Paragon
<Toast>takeschildren,action({ label, href?, onClick? }),show,onClose,closeLabel,delay.TabPagetoday passeschildren=toastHeader,action={ label: toastBodyText, href: toastBodyLink },show=!!toastHeader. Onlychildren+actioncarry per-toast content;showis now
isToastOpen, andcloseLabel/delayare constant. So:Writers → React Query mutations
New
src/course-home/data/apiHooks.ts(first RQ hook in that folder), followingthe repo's one established mutation recipe (
product-tours/data/apiHooks.ts):useMutation→mutationFncalls the existingapi.jsfn →onError: logError.Reuses
postCourseDeadlines/executePostFromPostEventunchanged.The two fire-and-forget thunks become
useResetDeadlinesandusePostEvent. Eachhook owns only the permanent behavior — the POST plus the two toast calls on
success — and a shared
toastFrom(response.data)maps the server{ header, link, link_text }toToastContent. The hook has zero Redux in it.Mutation variables are typed inline (
{ courseId, model },{ postData, researchEventData }); only the nested post-eventpostDatashape is pulled out asa small
PostDatatype, purely to keep thatmutationFnsignature under the linelimit — not a per-mutation
…Variablesinterface.Deletes both thunks (and the
resetDeadlinesre-export fromdata/index.js),plus
setCallToActionToastand the three toast fields fromslice.js.The one transitional Redux seam lives in the caller, not the hook
Each writer also refreshed model-store data on success —
dispatch(getTabData(courseId)), i.e.fetchDatesTab/fetchOutlineTab/fetchCourse. That data is still Redux (the model store) and doesn't convertuntil the later Phase 3 tab PRs / Phase 4 (courseware). So the refresh has to stay
a
dispatch(...)— an RQ→Redux call — for now.We keep that call out of the mutation hook and fire it from the caller's
mutate(vars, { onSuccess }). BothonSuccesses run (the hook's toast first, thenthe caller's refresh). This keeps
apiHooks.tsRedux-free from day one: when thatdata becomes an RQ query, delete the single caller line (or swap it for
invalidateQueries) and never touch the hook.ShiftDatesAlert.jsx:resetDeadlines.mutate({ courseId, model }, { onSuccess: () => dispatch(fetch(courseId)) })(model/fetchprops unchanged;dispatchfrom the existinguseDispatch).useIFrameBehavior.ts:postEvent.mutate({ postData, researchEventData }, { onSuccess: () => dispatch(fetchCourse(...)) }).processEvent's refresh (fetchCourse, courseware) is the longest-lived seam —courseware data converts much later than the course-home tabs.
Why not defer the toast until that data is RQ (toast-last)? Extracting the
toast only once
fetchDatesTab/fetchOutlineTab/fetchCourseare RQ querieswould avoid the seam entirely (the refresh would be
invalidateQueriesfrom dayone). We chose toast-first anyway:
TabPageis shared, so leaving the toast inRedux keeps every course-home tab Redux-coupled until courseware converts
(Phase 4, far off). Toast-first unblocks the tab conversions now; the price is one
clearly-temporary RQ→Redux line per caller, deleted as each data source converts.
processEventparse/guard moves intouseIFrameBehaviorThe postMessage parse/guard that lived in the
processEventthunk (pullresearch_event_dataout before camelCasing so it stays in the shape the backendexpects;
camelCaseObject; checkeventName === POST_EVENT) moves into a namedhandlePostMessageEventfunction in the hook, sowindow.onmessage = handlePostMessageEventis a bare reference rather than an inline block.eventTypesis exported from
course-home/data/thunks.jsfor the guard. This PR thereforetouches the courseware iframe hook, not only course-home.
Tests
ToastContext.test.tsx(new) — provider/hook unit tests (initial state,setToastContent,openToast/closeToast, throw-outside-provider); mirrorsCoursewareSearchContext.test.tsx.course-home/data/apiHooks.test.tsx(new) — the low-level coverage:renderHookhostsuseResetDeadlines/usePostEventwith an axios-levelMockAdapter on the POST URL (same style as the old
TabPagetoast test), andasserts the server response is mapped into
toastContent+isToastOpen, plus thePOST body and the
onError → logErrorpath (both mutations). This is where the oldTabPage "displays Learning Toast" data-path coverage relocated to, because the
response→content mapping now lives in the hook — keeping the mock at the network
level for parity rather than stubbing the api module. Mutations are driven with the
await act(async () => { await …mutateAsync(); })form fromproduct-tours/data/apiHooks.test.tsx.apiHooks.tslands at 100%.TabPage.test.jsx— the toast test is kept but narrowed to TabPage'sactual remaining job: "given a toast in the context, render it." It mocks
useToast(via a one-linemockUseToastfactory so the return shape is writtenonce and each call passes only overrides) rather than driving a POST through the
whole chain. Rationale: TabPage is now a pass-through renderer; testing the render
belongs here, testing the server-data→content mapping belongs in the hook test.
We rejected reconstructing the data path here with a contrived trigger component.
setupTest.js— the sharedrender()wrapper gains<ToastProvider>(TabPagenow calls
useToast).setupTest'srender()) thatrenders
TabPagemust add<ToastProvider>to that wrapper, sinceTabPagenow calls
useToast()and would otherwise throw. Found via the full run:dates-tab/DatesTab.test.jsx— wrapper gains<ToastProvider>; the "shiftdue dates" test now exercises the full path end-to-end (click →
useResetDeadlines→ context → TabPage renders the toast), the real coverage ofTabPage's render wiring.
discussion-tab/DiscussionTab.test.jsx— wrapper gains<ToastProvider>(renders
TabPageviaTabContainer).courseware/CoursewareContainer.test.jsx— wrapper gains<ToastProvider>(
CoursewareContainerrendersTabPagedirectly).course-home/data/slice.test.js— drop the toast fields from the localfixtures.
course-home/data/redux.test.js— remove theresetDeadlinesthunk test(thunk deleted; coverage →
apiHooks.test.tsx).useIFrameBehavior.test.js— the olddispatch(processEvent(...))assertionis invalid; mock
usePostEventand assert the parse/guard →mutate(...)(and itsonSuccessrefresh) instead. Test name kept ("registers an event handler toprocess fetchCourse events") — the handler still ends in a
fetchCourserefresh,so only the body changed; the name was never inaccurate.
Verification
nvm use && npm run types && npm run lint && npm test && npm run build— all green:types clean, lint clean, full suite 106 suites / 896 passing (3 pre-existing skips),
build succeeds.
git grepforsetCallToActionToast/toastHeader/toastBodyText/toastBodyLinkand for a survivingresetDeadlines/processEventthunk (or
index.jsre-export) both come back clean.Manual testing (seeding the toast triggers via edx-when)
Both toast triggers need a self-paced course with a missed suggested-schedule
deadline:
resetDeadlinespath.processEvent/post_eventpath.That state requires a graded subsection with a relative due date, which can't
be set through the course-authoring MFE:
frontend-app-authoringPR #976deliberately hid the release/due-date fields for self-paced courses and never added
a relative-date field (legacy Studio's
custom_relative_datesUI wasn't ported).So the data is seeded directly via
edx-whenin the CMS shell rather thanauthored in Studio:
course_experience.relative_dates(LMS waffle) for the course; confirm withRELATIVE_DATES_FLAG.is_enabled(course_key).modulestore().get_course(ck)→ chapters → sequentials, pick the one withgraded == True).edx_when.api.set_dates_for_course(ck, [(subsection, {'due': timedelta(weeks=1)})]).Schedule.start_date(~30 days) so the due date is in the past →dates_banner_info.missed_deadlines: true→ the button appears.Do not re-publish the course afterward — a publish re-syncs edx-when from the
blocks' own fields (which have no due date) and wipes the manually-seeded date.
Then compare master vs. this branch: identical toast header/action, identical
auto-hide + manual close, and the post-action data refresh (banner clears / dates
shift) still fires. The only intended differences are the two noted under the
decoupling section (content persists through the fade-out; failed POSTs now log).
Verified on both master and this branch (self-paced course, seeded as above).
Behavior matches, with one confirmed improvement: on master the toast text blanks
for a frame on auto-hide before fading (closing nulls the content while
show={!!toastHeader}is still animating out); on this branch the content persistsand it fades cleanly — a direct payoff of decoupling content from visibility.
Both writers were exercised end-to-end: the
resetDeadlinespath via thedates-tab "Shift due dates" banner (master + branch), and the
usePostEventpost_eventpath via the LMS-rendered in-unit missed-deadlines banner (branch).Coverage caveat: every manual run rendered a reset-deadlines payload —
a
headerwith no action link (the local env has no endpoint that returns adifferent CTA, and
post_eventin this setup also points at reset-deadlines). Sothe message-with-action variant (
toastContent.action→ the Paragon actionbutton) was not observed by eye; it rests on the unit tests
(
apiHooks.testmaps{ header, link, link_text }→action, and its"omits the toast action when the response has no link text" case;
TabPage.testrenders the action label) plus Paragon owning the present/absent-action render.
Given
TabPagemaps it in one line (action={toastContent?.action ?? null}), therisk is low, but note it wasn't manually confirmed.
Plan
Implementation plan (as approved — the type was later renamed `ToastMessage` → `ToastContent` during implementation; see the decision log)
Plan: Convert the CTA toast from Redux to a React
ToastProviderContext
Part of the Redux → React Query migration (Stage 1, issue #1946), Phase 3
(course-home), stacked on #1970. Standalone prerequisite PR before converting
any course-home tab.
Why this first: the course-home "call-to-action" toast is shared client
state. It lives in the
courseHomeRedux slice (toastHeader/toastBodyText/
toastBodyLink+ thesetCallToActionToastreducer), is read/rendered bythe shared
TabPage, and is written by two places:resetDeadlinesthunk → dispatched fromShiftDatesAlert(course-home: dates + outline)processEventthunk → dispatched fromuseIFrameBehavior.ts(courseware in-unit iframe)Because
TabPageis shared across every tab (and courseware), a tab can't be"converted" while it still reaches Redux for the toast. So we extract the toast
first, then the tabs convert with no toast bridge.
This PR de-Redux-es the toast path end to end — two moves: (1) the toast
client state → a React
ToastProvider; (2) the two POST writers that feed it(
resetDeadlines,processEvent) → React Query mutations whoseonSuccesssetsthe toast. The only Redux left in the path afterward is the transitional
dispatch(getTabData)refresh, which the later tab PRs turn intoinvalidateQueries.Toast landscape (verified): exactly one toast in the whole app — the
single Paragon
<Toast>inTabPage.jsx, backed only by those slice members.No other toast/notification state exists.
Design: a general
ToastProvider(state only)There's exactly one toast in the app today (the landscape note above), so
this isn't about future reuse — it's extracting the one toast out of Redux. The provider
owns only the toast state;
TabPagerenders the single<Toast>readingfrom it. (State in the provider, render in the consumer — same split as
CoursewareSearchContext.) It's namedToastProvider/useToastrather thanCTAToast…simply because nothing in the mechanism is CTA-specific — it holds aToastMessageand renders whatever it's handed — not because we're pre-buildingfor hypothetical future toasts.
Framing: the Redux slice today fuses content and visibility — one
show={!!toastHeader}derivation, cleared together to close. That fusion is thewart we're removing, not relocating into the provider. In the new design the
message and the open/closed state are fully independent:
toastMessage: ToastMessage | null— the current notification content, written/cleared on its own.isToastOpen: boolean— whether the<Toast>is showing, opened/closed on its own.Nothing couples them: setting the message does not open the toast, and
closing does not clear the message. A writer that wants to surface a
notification does two explicit things —
setToastMessage(...)thenopenToast()(e.g. an RQ mutation's
onSuccesscalls both). Paragon's auto-hide and the closebutton both fire
onClose, wired tocloseToast, which only flipsisToastOpen— the content stays in place so the toast fades out with content, not blank.
Shape — derived from the real APIs, not invented:
<Toast>(node_modules/@openedx/paragon/dist/Toast/index.js) takeschildren,action({ label, href?, onClick? }),show,onClose,closeLabel(defaults to intl "Close"),delay.TabPagecurrently passeschildren=toastHeader,action={ label: toastBodyText, href: toastBodyLink },show=!!toastHeader.children+actioncarry per-toast content;showis derived and the rest are constant. So the stored payload is exactly:Approach
New
src/generic/ToastContext.tsx(TypeScript; mirrorsCoursewareSearchContext—
useState+useMemo, hook throws outside provider):ToastProviderholds two independent state pieces and renders only{children}(no<Toast>):useToast()→{ toastMessage, setToastMessage, isToastOpen, openToast, closeToast }.toast-prefixed names so they read correctly destructured from a general hook (isOpen/showwould be ambiguous at the call site).Provider placement — mounted once at the route root in
src/index.jsx,wrapping
<Routes>(the only common ancestor of bothTabPageparents:TabContainerandCoursewareContainer, which rendersTabPagedirectly).TabPage.jsxrenders the<Toast>from context — keeps the render and itsToast/genericMessagesimports; only the source swaps from Redux touseToast():Drop the
useSelector(state.courseHome)toast fields and thesetCallToActionToastdispatch. (Leave its
errorMessage/courseHomeMetareads — out of scope.)Writers → React Query mutations — the two thunks are fire-and-forget POSTs
(
.thennot awaited, no.catch) that on successdispatch(getTabData(courseId))then
dispatch(setCallToActionToast(...)). These are server-state calls, so theybecome mutations in a new
src/course-home/data/apiHooks.ts(first RQ hook inthat folder), following the only established mutation recipe in the repo
(
src/product-tours/data/apiHooks.ts):useMutation→mutationFncalls theexisting
api.jsfn →onSuccess(_data, vars)→onError: logError. Reuse theexisting
postCourseDeadlines/executePostFromPostEventinapi.jsunchanged.The hook owns only the permanent behavior — POST + toast — and has zero Redux
in it:
onSuccessis exactly the decoupled "two things" (setToastMessagethenopenToast).No
queryKeys.ts/useQueryClientyet — nothing RQ to invalidate until the data converts.Transitional refresh lives in the caller, not the hook — the success refresh
(
fetchDatesTab/fetchOutlineTab/fetchCourse) writes into the Redux modelstore, which isn't converted until the later tab/courseware PRs. So the caller
fires it as a per-call
mutate(vars, { onSuccess }); bothonSuccesses run (thehook's toast first, then the caller's refresh). This keeps the new
apiHooks.tsRedux-free from day one: when that data becomes an RQ query, you delete the one
caller line (or swap it for
invalidateQueries) and never touch the hook.Call sites (each already uses
useDispatchtoday):src/course-home/suggested-schedule-messaging/ShiftDatesAlert.jsx:const resetDeadlines = useResetDeadlines();→onClick={() => resetDeadlines.mutate({ courseId, model }, { onSuccess: () => dispatch(fetch(courseId)) })}(model/fetchprops unchanged;dispatchfrom the existinguseDispatch).src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts:const postEvent = usePostEvent();. The postMessage parse/guard that lived inprocessEvent(rawresearch_event_data,camelCaseObject,eventName === POST_EVENT) moves into thewindow.onmessagehandler, which then callspostEvent.mutate({ postData, researchEventData }, { onSuccess: () => dispatch(fetchCourse(postData.bodyParams.courseId)) }).eventTypesis exported fromthunks.js(or relocated) for the guard. This PR therefore touches the courseware iframe hook, not only course-home.Delete
resetDeadlines+processEventfromthunks.jsand theirindex.jsre-exports; delete
setCallToActionToast+ the three toast fields fromslice.js.Slice cleanup —
src/course-home/data/slice.js: removetoastHeader/toastBodyText/toastBodyLinkfrom initial state and thesetCallToActionToastreducer + export.
Files
src/generic/ToastContext.tsx(+ToastContext.test.tsx)src/course-home/data/apiHooks.ts(+apiHooks.test.tsx) —useResetDeadlines,usePostEventmutationssrc/index.jsx— wrap<Routes>in<ToastProvider>src/tab-page/TabPage.jsx— render<Toast>fromuseToast()instead of thestate.courseHomereadssrc/course-home/data/thunks.js— deleteresetDeadlines+processEvent; exporteventTypesfor the iframe guardsrc/course-home/data/index.js— drop theresetDeadlinesre-exportsrc/course-home/suggested-schedule-messaging/ShiftDatesAlert.jsx—useResetDeadlines().mutate(...)src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts— parse/guard +usePostEvent().mutate(...)src/course-home/data/slice.js— removesetCallToActionToast+ the three toast fieldssrc/course-home/data/apiHooks.test.tsx—renderHook+mutate(...)foruseResetDeadlines/usePostEvent: assert the POST args and the toast calls (setToastMessage/openToast). ThegetTabDatarefresh is now the caller's concern, so it's asserted in the call-site tests below, not here. Absorbs the oldredux.test.jsresetDeadlinesPOST coverage.src/setupTest.js— add<ToastProvider>to the sharedrender()wrapper (TabPage now callsuseToast).src/course-home/dates-tab/DatesTab.test.jsx— add<ToastProvider>to its owncomponentwrapper; its "Shift due dates" → Toast assertion now runs throughuseResetDeadlines+ the provider render.src/tab-page/TabPage.test.jsx— wrap in<ToastProvider>; TabPage still renders the Toast (now fed from context), so its toast assertions stay.src/course-home/data/slice.test.js— drop the toast initial-state fields + thesetCallToActionToastreducer test.src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js— thedispatch(processEvent(...))assertion (~line 357) is now invalid; mockusePostEventand assert the parse/guard →mutate(...)instead.src/course-home/data/redux.test.js— remove theresetDeadlinesthunk test (thunk deleted; coverage moves toapiHooks.test.tsx).decisions.md(repo root, untracked) — decision log for the PR body: single-toast finding; general state-onlyToastProvider,TabPagerenders; message and open/close state decoupled (no auto-open on set); shape derived from Paragon's Toast API; writers → RQ mutations owning only POST + toast (Redux-free), with the transitional model-store refresh kept in the caller'smutateonSuccessso the hook never carries Redux; and why this touches the courseware iframe hook.Verification
nvm use && npm run types && npm run lint && npm test(targeted first:TabPage,DatesTab,slice,redux,useIFrameBehavior, newToastContextandapiHookstests), thennpm run build.git grep -n "setCallToActionToast\|toastHeader\|toastBodyText\|toastBodyLink" src→ none in source; slice carries no toast state.git grep -n "resetDeadlines\|processEvent" src→ only the newapiHooks.ts(useResetDeadlines/usePostEvent) and its callers/tests; no surviving thunk orindex.jsre-export.gh stack addonto refactor: convert courseware search from Redux to React Query #1970, open the stacked PR as a draft, improve coverage, mark ready on your go.Closes #1980
🤖 Generated with Claude Code