FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: move discussion topic prefetch from trigger to widget config lifecycle by awais-ansari · Pull Request #1897 · openedx/frontend-app-learning · GitHub

feat: move discussion topic prefetch from trigger to widget config lifecycle - #1897

Merged
arbrandes merged 3 commits into
openedx:masterfrom
awais-ansari:aansari/clean-up-hide-upsell
Apr 14, 2026
Merged

arbrandes merged 3 commits into
openedx:masterfrom
awais-ansari:aansari/clean-up-hide-upsell

Conversation

Copy link
Copy Markdown
Contributor

Summary

Moves data-fetching logic (getCourseDiscussionTopics dispatch) out of DiscussionsTrigger and into a new prefetch field on the widget config. SidebarContextProvider now runs widget.prefetch() for all enabled widgets on mount and when course metadata changes, ensuring Redux store data is populated before isAvailable checks and component rendering.

Motivation

Previously, DiscussionsTrigger was responsible for both rendering and fetching discussion topics. This created a coupling between the trigger component and the data lifecycle. The trigger had to mount before data was available, causing timing issues with widget availability checks. Moving prefetch to the framework level ensures data is ready before any widget logic runs.

Changes

Code (5 files)

  • SidebarContextProvider.jsx: Added useDispatch + useEffect that iterates enabledWidgets and calls widget.prefetch({ courseId, course, dispatch }) when course metadata changes.
  • DiscussionsTrigger.jsx: Removed useDispatch, useEffect, getCourseDiscussionTopics, courseHomeMeta tabs lookup, and getConfig().DISCUSSIONS_MFE_BASE_URL check. Now a pure render component that only reads the discussionTopics model.
  • widgetConfig.js: Added discussionsPrefetch function and prefetch field to the discussions widget config.
  • SidebarContextProvider.test.jsx: Added react-redux mock, enriched useModel mock for courseHomeMeta, updated UC7 toggle tests to reflect mobile behavior (no auto-open).
  • DiscussionsTrigger.test.jsx: Added executeThunk(getCourseDiscussionTopics(...)) in beforeEach to pre-populate the store.

Documentation (4 files)

  • ARCHITECTURE.md: Documented the prefetch lifecycle and updated provider responsibilities.
  • README.md: Added prefetch to the widget structure definition and usage guide.
  • USE_CASE_VERIFICATION.md: Added Prefetch Effect (effect #0) to the architecture diagram.
  • discussions/README.md: Added discussionsPrefetch export and data prefetch documentation.

Widget Config API Addition

The prefetch field is a new optional field on the widget config:

{
  id: 'MY_WIDGET',
  prefetch: ({ courseId, course, dispatch }) => {
    dispatch(fetchMyData(courseId));
  },
  // ... other fields
}

awais-ansari requested a review from arbrandes April 13, 2026 18:57

codecov Bot commented Apr 13, 2026 •
edited
Loading

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.28%. Comparing base (0664dc3) to head (acc6ee6).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1897   +/-   ##
=======================================
  Coverage   91.27%   91.28%           
=======================================
  Files         343      343           
  Lines        5766     5770    +4     
  Branches     1387     1388    +1     
=======================================
+ Hits         5263     5267    +4     
  Misses        484      484           
  Partials       19       19           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Pull request overview

This PR moves Discussions data loading (getCourseDiscussionTopics) out of the DiscussionsTrigger component and into a new optional prefetch hook on sidebar widget configs, executed by SidebarContextProvider when the provider mounts and when course metadata updates.

Changes:

  • Add widget.prefetch({ courseId, course, dispatch }) execution in SidebarContextProvider.
  • Introduce discussionsPrefetch + prefetch in the discussions widget config; simplify DiscussionsTrigger to render-only.
  • Update tests and docs to reflect the new widget prefetch lifecycle; add a new unit test file for Sidebar.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src/courseware/course/sidebar/SidebarContextProvider.jsx Runs prefetch for enabled widgets via a new effect.
src/widgets/discussions/widgetConfig.js Adds discussionsPrefetch and wires it into the widget config.
src/widgets/discussions/DiscussionsTrigger.jsx Removes fetching logic; now reads the model and renders conditionally.
src/widgets/discussions/DiscussionsTrigger.test.jsx Pre-populates the store by executing the thunk in beforeEach.
src/courseware/course/sidebar/SidebarContextProvider.test.jsx Mocks useDispatch/useModel and adjusts UC7 toggle expectations.
src/courseware/course/sidebar/Sidebar.test.jsx Adds unit tests for Sidebar rendering/switching behavior.
src/courseware/course/sidebar/README.md Documents the new prefetch widget config field.
src/courseware/course/sidebar/ARCHITECTURE.md Documents the provider’s prefetch responsibility and lifecycle.
src/courseware/course/sidebar/USE_CASE_VERIFICATION.md Updates the architecture diagram to include a prefetch effect.
src/widgets/discussions/README.md Documents discussions-specific prefetch behavior and exports.
Comments suppressed due to low confidence (1)

src/widgets/discussions/DiscussionsTrigger.jsx:13

  • DiscussionsTrigger no longer reads DISCUSSIONS_MFE_BASE_URL, but it still calls ensureConfig(['DISCUSSIONS_MFE_BASE_URL']) at module load. This keeps an import-time hard dependency on that config even when the trigger is meant to be a pure render component. Consider moving the ensureConfig call to where the config is actually used now (e.g., discussionsPrefetch and/or DiscussionsSidebar) and removing it from the trigger.
import { ensureConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@openedx/paragon';
import { QuestionAnswer } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { useContext } from 'react';
import { useModel } from '@src/generic/model-store';
import { WIDGETS } from '@src/constants';
import SidebarTriggerBase from '@src/courseware/course/sidebar/common/TriggerBase';
import SidebarContext from '@src/courseware/course/sidebar/SidebarContext';
import messages from './messages';

ensureConfig(['DISCUSSIONS_MFE_BASE_URL']);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/courseware/course/sidebar/README.md Outdated
Comment thread src/widgets/discussions/README.md Outdated

arbrandes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

At first glance this makes a lot of sense: centralizing data prefetch in the provider so that isAvailable checks can rely on pre-populated Redux state. But there's an issue:

The prefetch effect at SidebarContextProvider.jsx#L89-L96 includes coursewareMeta and courseHomeMeta in its dependency array. The two models settle in separate React batches during initial load, which causes the effect to fire twice after the guard in discussionsPrefetch starts passing.

I confirmed this produces duplicate requests to both GET /api/discussion/v1/courses/{courseId} and GET /api/discussion/v2/course_topics/{courseId} on every page load - two requests to each endpoint returning identical data (thunks.js#L268-L286).

The old code in DiscussionsTrigger avoided this because its effect depended on [courseId, baseUrl, edxProvider, dispatch], all of which were stable after the initial render.

A simple fix might be to remove coursewareMeta and courseHomeMeta from the dependency array. The effect only needs to run once per courseId - the course object is only used inside discussionsPrefetch to check tabs, and that check can happen with the latest value via a ref or by passing the models lazily.

awais-ansari requested a review from arbrandes April 13, 2026 20:22

arbrandes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Ok, the ref workaround solves it, but it's a bit hacky. The real solution is going to be moving to React Query, later on.

Let's worry about that later, though! 👍🏼

arbrandes merged commit 59113b0 into openedx:master Apr 14, 2026
7 checks passed
arbrandes added a commit to arbrandes/frontend-app-learning that referenced this pull request Apr 16, 2026
arbrandes added a commit to arbrandes/frontend-app-learning that referenced this pull request Apr 16, 2026
Reverts the following two commits:

- 59113b0 feat: move discussion topic prefetch from trigger to widget
  config lifecycle (openedx#1897)
- 0664dc3 feat: decouple notifications panel using widget registry
  mechanism (openedx#1885)

Co-Authored-By: Claude <noreply@anthropic.com>
brian-smith-tcril added a commit that referenced this pull request Sep 24, 2026
…t it to TypeScript

The courseware sidebar provider becomes TypeScript, and the two patterns in
it that predate React Query go with the conversion instead of being typed
around. The context was created with a default object, so a consumer rendered
outside `SidebarProvider` got empty values instead of an error; it is now
`createContext<SidebarContextValue | null>(null)` with a `useSidebarContext()`
hook that throws outside the provider. The provider ran every widget's
`prefetch` from an effect that read the merged course metadata through a ref,
PR #1897's fix for the effect firing twice as the two metadata models settled
in separate batches, approved then as a workaround pending React Query.

The one `prefetch` user, the discussions widget, now loads its topics as a
query observer in a `Provider` component, mounted by the framework for every
enabled widget inside `SidebarContext`, with `enabled` set only when the
discussions MFE is configured and the course has a discussion tab. An observer
fetches on mount and on key change, not when the metadata it reads
re-renders, so one sidebar mount is one topics request and a metadata write
is none. The query definition is a `queryOptions` object,
`discussionTopicsQuery`, shared with the tests and with #2087's reader hook;
its bridge `meta` stays until that layer converts the three `useModel`
readers. The effect, `courseMetaRef`, the duplicated merge and the `prefetch`
field of the widget contract are gone.

The widget contract gets named types in `SidebarContext.ts`: `SidebarWidget`,
`SidebarWidgetContext` (`course: CourseHomeMeta & CoursewareMeta`,
`unit: Partial<DiscussionTopic>`, the line #2087 changes) and
`SidebarContextValue`. `CoursewareMeta` and `DiscussionTopic` are declared
beside their queries. Both built-in widget configs are TypeScript and declared
against `SidebarWidget`; the upgrade widget's context module converts with
them. The sidebar README's contract sections point at the declarations
instead of restating them, and the prefetch section describes the `Provider`
observer.

Tests: `DiscussionsProvider.test.tsx` measures the observer's gates as
requests and pins that a course-metadata write does not refetch the topics;
`SidebarContext.test.tsx` covers the hook's throw; `LockPaywall`,
`SequenceNavigation` and `SequenceNavigationTabs` render their context
consumer under a provider, which the default value had let them skip.

BREAKING CHANGE: the `prefetch` field of a `SIDEBAR_WIDGETS` entry is no
longer called. A widget that loaded data through it loads it in its
`Provider` component with `useQuery` and `enabled`; see "Loading widget data"
in src/courseware/course/sidebar/README.md. `useContext(SidebarContext)`
returns `null` outside `SidebarProvider`; read the context with
`useSidebarContext()` from src/courseware/course/sidebar/SidebarContext.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril added a commit that referenced this pull request Sep 24, 2026
…t it to TypeScript

The courseware sidebar provider becomes TypeScript, and the two patterns in
it that predate React Query go with the conversion instead of being typed
around. The context was created with a default object, so a consumer rendered
outside `SidebarProvider` got empty values instead of an error; it is now
`createContext<SidebarContextValue | null>(null)` with a `useSidebarContext()`
hook that throws outside the provider. The provider ran every widget's
`prefetch` from an effect that read the merged course metadata through a ref,
PR #1897's fix for the effect firing twice as the two metadata models settled
in separate batches, approved then as a workaround pending React Query.

The one `prefetch` user, the discussions widget, now loads its topics as a
query observer in a `Provider` component, mounted by the framework for every
enabled widget inside `SidebarContext`, with `enabled` set only when the
discussions MFE is configured and the course has a discussion tab. An observer
fetches on mount and on key change, not when the metadata it reads
re-renders, so one sidebar mount is one topics request and a metadata write
is none. The query definition is a `queryOptions` object,
`discussionTopicsQuery`, shared with the tests and with #2087's reader hook;
its bridge `meta` stays until that layer converts the three `useModel`
readers. The effect, `courseMetaRef`, the duplicated merge and the `prefetch`
field of the widget contract are gone.

The widget contract gets named types in `SidebarContext.ts`: `SidebarWidget`,
`SidebarWidgetContext` (`course: CourseHomeMeta & CoursewareMeta`,
`unit: Partial<DiscussionTopic>`, the line #2087 changes) and
`SidebarContextValue`. `CoursewareMeta` and `DiscussionTopic` are declared
beside their queries. Both built-in widget configs are TypeScript and declared
against `SidebarWidget`; the upgrade widget's context module converts with
them. The sidebar README's contract sections point at the declarations
instead of restating them, and the prefetch section describes the `Provider`
observer.

Tests: `DiscussionsProvider.test.tsx` measures the observer's gates as
requests and pins that a course-metadata write does not refetch the topics;
`SidebarContext.test.tsx` covers the hook's throw; `LockPaywall`,
`SequenceNavigation` and `SequenceNavigationTabs` render their context
consumer under a provider, which the default value had let them skip.

BREAKING CHANGE: the `prefetch` field of a `SIDEBAR_WIDGETS` entry is no
longer called. A widget that loaded data through it loads it in its
`Provider` component with `useQuery` and `enabled`; see "Loading widget data"
in src/courseware/course/sidebar/README.md. `useContext(SidebarContext)`
returns `null` outside `SidebarProvider`; read the context with
`useSidebarContext()` from src/courseware/course/sidebar/SidebarContext.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril added a commit that referenced this pull request Sep 24, 2026
…t it to TypeScript

The courseware sidebar provider becomes TypeScript, and the two patterns in
it that predate React Query go with the conversion instead of being typed
around. The context was created with a default object, so a consumer rendered
outside `SidebarProvider` got empty values instead of an error; it is now
`createContext<SidebarContextValue | null>(null)` with a `useSidebarContext()`
hook that throws outside the provider. The provider ran every widget's
`prefetch` from an effect that read the merged course metadata through a ref,
PR #1897's fix for the effect firing twice as the two metadata models settled
in separate batches, approved then as a workaround pending React Query.

The one `prefetch` user, the discussions widget, now loads its topics as a
query observer in a `Provider` component, mounted by the framework for every
enabled widget inside `SidebarContext`, with `enabled` set only when the
discussions MFE is configured and the course has a discussion tab. An observer
fetches on mount and on key change, not when the metadata it reads
re-renders, so one sidebar mount is one topics request and a metadata write
is none. The query definition is a `queryOptions` object,
`discussionTopicsQuery`, shared with the tests and with #2087's reader hook;
its bridge `meta` stays until that layer converts the three `useModel`
readers. The effect, `courseMetaRef`, the duplicated merge and the `prefetch`
field of the widget contract are gone.

The widget contract gets named types in `SidebarContext.ts`: `SidebarWidget`,
`SidebarWidgetContext` (`course: CourseHomeMeta & CoursewareMeta`,
`unit: Partial<DiscussionTopic>`, the line #2087 changes) and
`SidebarContextValue`. `CoursewareMeta` and `DiscussionTopic` are declared
beside their queries. Both built-in widget configs are TypeScript and declared
against `SidebarWidget`; the upgrade widget's context module converts with
them. The sidebar README's contract sections point at the declarations
instead of restating them, and the prefetch section describes the `Provider`
observer.

Tests: `DiscussionsProvider.test.tsx` measures the observer's gates as
requests and pins that a course-metadata write does not refetch the topics;
`SidebarContext.test.tsx` covers the hook's throw; `LockPaywall`,
`SequenceNavigation` and `SequenceNavigationTabs` render their context
consumer under a provider, which the default value had let them skip.

The provider's first render now reads `useWindowSize().width ?? window.innerWidth`
instead of an undefined width. That fixes a bug present since #1885: on a
narrow viewport the first render seeded the sidebar closed, and the mobile
branches of the sidebar hooks never corrected it, so a panel a learner had
opened did not restore after a refresh. Desktop reaches the same state as
before, without a transient outline render and two storage writes per load.

BREAKING CHANGE: the `prefetch` field of a `SIDEBAR_WIDGETS` entry is no
longer called. A widget that loaded data through it loads it in its
`Provider` component with `useQuery` and `enabled`; see "Loading widget data"
in src/courseware/course/sidebar/README.md. `useContext(SidebarContext)`
returns `null` outside `SidebarProvider`; read the context with
`useSidebarContext()` from src/courseware/course/sidebar/SidebarContext.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
brian-smith-tcril added a commit that referenced this pull request Sep 28, 2026
…t it to TypeScript (#2113)

The courseware sidebar provider becomes TypeScript, and the two patterns in
it that predate React Query go with the conversion instead of being typed
around. The context was created with a default object, so a consumer rendered
outside `SidebarProvider` got empty values instead of an error; it is now
`createContext<SidebarContextValue | null>(null)` with a `useSidebarContext()`
hook that throws outside the provider. The provider ran every widget's
`prefetch` from an effect that read the merged course metadata through a ref,
PR #1897's fix for the effect firing twice as the two metadata models settled
in separate batches, approved then as a workaround pending React Query.

The one `prefetch` user, the discussions widget, now loads its topics as a
query observer in a `Provider` component, mounted by the framework for every
enabled widget inside `SidebarContext`, with `enabled` set only when the
discussions MFE is configured and the course has a discussion tab. An observer
fetches on mount and on key change, not when the metadata it reads
re-renders, so one sidebar mount is one topics request and a metadata write
is none. The query definition is a `queryOptions` object,
`discussionTopicsQuery`, shared with the tests and with #2087's reader hook;
its bridge `meta` stays until that layer converts the three `useModel`
readers. The effect, `courseMetaRef`, the duplicated merge and the `prefetch`
field of the widget contract are gone.

The widget contract gets named types in `SidebarContext.ts`: `SidebarWidget`,
`SidebarWidgetContext` (`course: CourseHomeMeta & CoursewareMeta`,
`unit: Partial<DiscussionTopic>`, the line #2087 changes) and
`SidebarContextValue`. `CoursewareMeta` and `DiscussionTopic` are declared
beside their queries. Both built-in widget configs are TypeScript and declared
against `SidebarWidget`; the upgrade widget's context module converts with
them. The sidebar README's contract sections point at the declarations
instead of restating them, and the prefetch section describes the `Provider`
observer.

Tests: `DiscussionsProvider.test.tsx` measures the observer's gates as
requests and pins that a course-metadata write does not refetch the topics;
`SidebarContext.test.tsx` covers the hook's throw; `LockPaywall`,
`SequenceNavigation` and `SequenceNavigationTabs` render their context
consumer under a provider, which the default value had let them skip.

The provider's first render now reads `useWindowSize().width ?? window.innerWidth`
instead of an undefined width. That fixes a bug present since #1885: on a
narrow viewport the first render seeded the sidebar closed, and the mobile
branches of the sidebar hooks never corrected it, so a panel a learner had
opened did not restore after a refresh. Desktop reaches the same state as
before, without a transient outline render and two storage writes per load.

BREAKING CHANGE: the `prefetch` field of a `SIDEBAR_WIDGETS` entry is no
longer called. A widget that loaded data through it loads it in its
`Provider` component with `useQuery` and `enabled`; see "Loading widget data"
in src/courseware/course/sidebar/README.md. `useContext(SidebarContext)`
returns `null` outside `SidebarProvider`; read the context with
`useSidebarContext()` from src/courseware/course/sidebar/SidebarContext.ts.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants


Back | FazBrowse Home | New Git URL