Skip to content

fix: move module-scope Emitter in react.tsx into the Provider - #413

Open
EngineerOnTravel wants to merge 1 commit into
Flagsmith:mainfrom
EngineerOnTravel:fix-issue-391
Open

fix: move module-scope Emitter in react.tsx into the Provider#413
EngineerOnTravel wants to merge 1 commit into
Flagsmith:mainfrom
EngineerOnTravel:fix-issue-391

Conversation

@EngineerOnTravel

Copy link
Copy Markdown

Move module-scope Emitter in react.tsx into the Provider

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature. (not applicable - this is an internal bugfix with no public API or docs change)
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Closes #391

react.tsx instantiated a single Emitter at module scope:

const events = new Emitter()

Every <FlagsmithProvider> tree in the process shared that one event bus.
On long-lived Node hosts (Fluid Compute, serverful SSR, or any app that
mounts more than one FlagsmithProvider), an internal flag update on one
provider's flagsmith instance would emit on the shared events object and
notify useFlags/useFlagsmithLoading/useExperiment hooks belonging to
completely unrelated provider trees. This was safe today only by luck,
because useFlags re-computes a render key from its own flagsmith
instance and bails out when the value hasn't changed - but it's still
unnecessary cross-request/cross-tree coupling and wasted re-render work.

This PR moves the Emitter out of module scope and into a useRef owned by
FlagsmithProvider, and threads it to the hooks via a new internal
EventsContext provided alongside the existing FlagsmithContext:

  • FlagsmithProvider now creates one Emitter per mounted provider
    instance (useRef, lazily initialized) instead of reusing a shared
    module-level singleton, and provides it via EventsContext.
  • useFlagsmithLoading, useFlags, and useExperiment now read the
    emitter from EventsContext instead of closing over the old module-level
    events object, and guard their effects on the emitter being present
    (mirrors the existing !flagsmith guards).
  • EventsContext is internal (not exported) - this is purely an
    implementation detail fix with no public API change.

How did you test this code?

Verified with the repo's own CI commands (.github/workflows/pull-request.yml
runs npm i && npm run build && npm test):

$ npm i
$ npm run build   # runs prebuild, rollup bundle, and `tsc` typecheck - exits 0, no errors
$ npm test        # jest --env=jsdom
Test Suites: 19 passed, 19 total
Tests:       121 passed, 121 total

I also added a new regression test,
test/react-provider-isolation.test.tsx, which renders two independent
FlagsmithProvider trees (two separate flagsmith instances) and asserts
that firing one instance's internal _trigger() (the same call
flagsmith-core makes after a real flag update) only re-renders the
useFlags consumer under that provider, and never touches the sibling
provider's consumer. I confirmed this test fails against the pre-fix code
(the sibling's useFlags consumer spuriously re-renders because both trees
shared the module-scope events emitter) and passes with the fix.

@EngineerOnTravel
EngineerOnTravel requested a review from a team as a code owner August 18, 2026 04:28
@EngineerOnTravel
EngineerOnTravel requested review from kyle-ssg and removed request for a team August 18, 2026 04:28
@EngineerOnTravel EngineerOnTravel changed the title Move module-scope Emitter in react.tsx into the Provider fix: move module-scope Emitter in react.tsx into the Provider Aug 18, 2026
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.

Move module-scope Emitter in react.tsx into the Provider

1 participant