Skip to content

feat(website): Sync state changes across tabs#4174

Merged
ravern merged 10 commits intonusmodifications:masterfrom
TheMythologist:sync-across-tabs
Feb 15, 2026
Merged

feat(website): Sync state changes across tabs#4174
ravern merged 10 commits intonusmodifications:masterfrom
TheMythologist:sync-across-tabs

Conversation

@TheMythologist
Copy link
Copy Markdown
Contributor

Context

Previously, changes to a redux state were not reflected in other tabs. This causes 2 main issues:

  1. Major issue: Scenario where 2 tabs are opened to different modules. Clicking "Add to Semester ..." button in 1 tab, followed by the "Add to Semester ..." button in the other tab, will only cause the 2nd module to be added. (The change in the second tab overrode the change in the first tab).
  2. Minor issue: Scenario where 2 tabs are opened, 1 to the timetable and another tab to a module. After clicking "Add to Semester..." button in the module tab, you have to manually refresh the timetable tab for this module to appear.

Implementation

Uses redux-state-sync to sync redux states across tabs, which uses a polyfilled BroadcastChannel under the hood.

Other Information

Another option I looked at was redux-persist-crosstab, which is unmaintained and incompatible with the current version of redux-persist.

Additionally, I had to forcefully resolve broadcast-channel to version 5.3.0.

@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 17, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nusmods-export Ready Ready Preview, Comment Feb 15, 2026 2:37am
nusmods-website Ready Ready Preview, Comment Feb 15, 2026 2:37am

Request Review

@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 17, 2025

@TheMythologist is attempting to deploy a commit to the modsbot's projects Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Member

@zwliew zwliew left a comment

Choose a reason for hiding this comment

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

Thanks for looking into this! The change looks good to me with a minor nit.

Do let one of the other active maintainers @ravern, @jloh02, or @leslieyip02 review it too before merging.

Comment thread website/src/middlewares/state-sync-middleware.ts Outdated
Comment thread website/package.json Outdated
Comment thread website/package.json Outdated
Comment thread website/package.json Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Aug 31, 2025

Codecov Report

❌ Patch coverage is 40.00000% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.81%. Comparing base (988c6fd) to head (1310be2).
⚠️ Report is 170 commits behind head on master.

Files with missing lines Patch % Lines
website/src/middlewares/state-sync-middleware.ts 28.57% 5 Missing ⚠️
website/src/middlewares/requests-middleware.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4174      +/-   ##
==========================================
+ Coverage   54.52%   56.81%   +2.28%     
==========================================
  Files         274      298      +24     
  Lines        6076     6942     +866     
  Branches     1455     1676     +221     
==========================================
+ Hits         3313     3944     +631     
- Misses       2763     2998     +235     

☔ 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.

@TheMythologist
Copy link
Copy Markdown
Contributor Author

Hi @zwliew I finally got to working on this again, could you help me take a look at the changes and review this PR once more? Thanks!

@TheMythologist TheMythologist requested a review from zwliew August 31, 2025 13:21
Copy link
Copy Markdown
Member

@jloh02 jloh02 left a comment

Choose a reason for hiding this comment

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

hi i noticed a bug. steps to reproduce

  1. Open 2 tabs on https://nusmods-website-mmnouxt0t-modsbots-projects.vercel.app/timetable/sem-1
  2. Add a course on one tab
  3. Other tab has the screenshot as below
image

}

// `FETCH_` request actions should not be synced to other tabs
if (action.type.toString().startsWith('FETCH_')) {
Copy link
Copy Markdown
Member

@zwliew zwliew Sep 7, 2025

Choose a reason for hiding this comment

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

hook.js:608 TypeError: Cannot read properties of undefined (reading 'find')
    at u (modules.ts:30:30)
    at d (modules.ts:35:16)
    at timetables.ts:168:21
    at mapValues.js:38:34
    at _createBaseFor.js:17:11
    at e.exports (_baseForOwn.js:13:20)
    at e.exports (mapValues.js:37:3)
    at ue (timetables.ts:167:10)
    at timetables.ts:127:14

The problem is that in getModuleSemesterData() of modules.ts, the semesterData field of module is undefined for a module that was synced over from another tab.

Perhaps a fix for this is to whitelist the FETCH_MODULE_REQUEST and FETCH_MODULE_SUCCESS actions, which are there to add module info (like semesterData) for newly added modules.

As an aside, I would be more comfortable with switching over to an action whitelist rather than a blacklist.

@TheMythologist
Copy link
Copy Markdown
Contributor Author

@jloh02 @zwliew Finally got to fixing the issue. The reason for the weird filtering by requests starting with "FETCH_" was ping-pong responses between tabs whenever any fetch request was started, resulting in actions with type FETCH_MODULE_REQUEST_REQUEST_REQUEST_REQUEST_REQUEST_REQUEST_SUCCESS. The fix was in the requests middleware, to prevent ongoing requests from being propagated again.

@ravern
Copy link
Copy Markdown
Member

ravern commented Feb 11, 2026

Thanks for the recent fix!

Just as a first pass, I'm going to enable an AI code review tool. But we (humans) will still be the ones reviewing and approving later on.

@greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 11, 2026

Greptile Overview

Confidence Score: 4/5

  • This PR is safe to merge with minor verification needed on dependency resolution
  • The implementation correctly addresses cross-tab synchronization with proper safeguards against infinite loops. The code quality is good with appropriate filtering logic and test mocking. Score reduced by 1 due to discrepancy between PR description (mentions forcing broadcast-channel to v5.3.0) and actual package.json (no such resolution present), which should be verified before merging.
  • Verify package.json - PR description mentions forcing broadcast-channel to v5.3.0, but this resolution is not present in the resolutions field

Important Files Changed

Filename Overview
website/src/middlewares/state-sync-middleware.ts Added new middleware to sync Redux state across browser tabs using redux-state-sync, with proper filtering for redux-persist actions and non-serializable functions
website/src/middlewares/requests-middleware.ts Added guard to prevent re-processing API request actions that already have requestStatus, preventing ping-pong sync loops between tabs
website/src/bootstrapping/configure-store.ts Integrated the new state sync middleware into the Redux store configuration
website/package.json Added dependencies for redux-state-sync, @types/redux-state-sync, and @jest/globals to support cross-tab state synchronization

Sequence Diagram

sequenceDiagram
    participant Tab1 as Tab 1
    participant BC as BroadcastChannel
    participant Tab2 as Tab 2
    participant Redux1 as Redux Store (Tab 1)
    participant Redux2 as Redux Store (Tab 2)

    Tab1->>Redux1: Dispatch Action (e.g., Add Module)
    Redux1->>Redux1: requestsMiddleware filters actions with requestStatus
    Redux1->>Redux1: stateSyncMiddleware predicate checks action
    Note over Redux1: Filters out PERSIST/PURGE/REHYDRATE<br/>and function actions
    Redux1->>BC: Broadcast action to other tabs
    Redux1->>Redux1: Update local state
    BC->>Tab2: Receive broadcasted action
    Tab2->>Redux2: Dispatch received action
    Redux2->>Redux2: requestsMiddleware skips re-processing<br/>(prevents ping-pong)
    Redux2->>Redux2: Update state with synced action
    Note over Tab1,Tab2: Both tabs now have synchronized state
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 11, 2026

Additional Comments (1)

website/package.json
PR description mentions forcefully resolving broadcast-channel to version 5.3.0, but this resolution is missing from package.json. The yarn.lock shows versions 2.3.4 and 3.7.0 are being used instead. Verify if this needs to be added:

  "resolutions": {
    "broadcast-channel": "5.3.0",
    "cheerio": "1.0.0-rc.12"
  }

@TheMythologist
Copy link
Copy Markdown
Contributor Author

I can't recall why I initially locked the version of broadcast-channel package to 5.3.0, and that commit was too long ago that I forgot as well 😅

That being said, the current version that is installed (3.7.0) is pretty old. I did some local testing by fixing broadcast-channel to the latest version (7.3.0) and it works fine, looking at the changelog nothing that is used by broadcast-channel is affected. Should I fix it to the latest version? @ravern

Copy link
Copy Markdown
Member

@ravern ravern left a comment

Choose a reason for hiding this comment

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

LGTM overall :D

Thank you for implementing this, and apologies for the insanely long review time.

Regarding the broadcast-channel, since it is a dep of a dep, I'd rather just leave it at whatever version redux-state-sync specifies. Unless there's a security flaw that warrants a version bump.

Fix the build errors, and I'll be happy to approve and merge this in!

@TheMythologist
Copy link
Copy Markdown
Contributor Author

@ravern thanks for the review! No worries, it took me just as long to work on it as well 😅

Regarding the build failures, what specifically is failing? If you're referring to the typecheck failures, those files are not touched by me so I'm not sure what you mean. I tried yarn build locally and it seems to be working fine for me. I also can't exactly see the vercel build logs so I'm not sure what went wrong, perhaps you could copy paste the logs from there so I can see what needs to be fixed?

@ravern
Copy link
Copy Markdown
Member

ravern commented Feb 15, 2026

For the build failure, I was referring to the CircleCI workflow, which is publicly-accessible. This is the one that's failing: https://app.circleci.com/pipelines/github/nusmodifications/nusmods/16305/workflows/f5019a8b-cee9-49c4-9d6e-efb647bc40ba/jobs/96459

@ravern
Copy link
Copy Markdown
Member

ravern commented Feb 15, 2026

Yeah it doesn't look like any files you've touched.

I will take over this PR to fix the unrelated issues + get it merged!

ravern and others added 2 commits February 15, 2026 10:20
The regenerated yarn.lock pulled in @types/react@19 as a nested
dependency of @types/react-router, causing TypeScript build errors.
Reset to master's lockfile and reinstall to keep correct deduplication.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@ravern ravern left a comment

Choose a reason for hiding this comment

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

LGTM since build passes now.

@ravern ravern merged commit 307fa2a into nusmodifications:master Feb 15, 2026
5 of 6 checks passed
@TheMythologist TheMythologist deleted the sync-across-tabs branch February 15, 2026 03:26
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.

4 participants