Skip to content

[DevTools] Ignore new production renderers if we already use "worse" versions of React on a page#679

Closed
everettbu wants to merge 2 commits into
mainfrom
sebbie/03-10-_devtools_ignore_new_production_renderers_if_we_already_use_worse_versions_of_react_on_a_page
Closed

[DevTools] Ignore new production renderers if we already use "worse" versions of React on a page#679
everettbu wants to merge 2 commits into
mainfrom
sebbie/03-10-_devtools_ignore_new_production_renderers_if_we_already_use_worse_versions_of_react_on_a_page

Conversation

@everettbu

Copy link
Copy Markdown

Mirror of facebook/react#35994
Original author: eps1lon


Summary

When a new build of React is detected on the screen, React DevTools will display a special icon and extension popup for this build. This helps discover if a page is accidentally using a development build.

However, right now the last seen React build is considered. This can hide if a React development build on the page is used.

Now we keep the seen React builds in state and only change the icon/popup if we see a new, worse React builds. Worse is is just any non-production build.

Originally reported in vercel/next.js#90744

How did you test this change?

  • Start a Next.js app in dev (e.g. npm run dev next) to ensure the development build icon is used
    CleanShot 2026-03-10 at 18 44 40`@2x`

@everettbu everettbu added CLA Signed React Core Team Opened by a member of the React Core Team labels Mar 10, 2026
@everettbu
everettbu marked this pull request as ready for review March 10, 2026 19:27
@greptile-apps-staging

Copy link
Copy Markdown

Greptile Summary

This PR fixes a bug where React DevTools would display the wrong extension icon/popup when a page uses multiple React renderers — specifically, a later production renderer could overwrite a previously-detected non-production (development/unminified/etc.) build, hiding the fact that a dev build was in use. The fix introduces a reduceReactBuild accumulator (in a new reactBuildType.js content-script module) that treats any non-production build as "worse" than production and refuses to downgrade once one is detected. It also centralises the ReactBuildType union type, adds Flow types to previously-untyped call sites, and adds missing ExtensionAction/getURL declarations to the global Flow stubs.

  • New file reactBuildType.js: introduces reduceReactBuild (accumulator logic) and createReactRendererListener (stateful closure that drives postMessage to the background script).
  • installHook.js: replaces the inline renderer event handler with createReactRendererListener(window).
  • types.js: exports the new ReactBuildType union type, used throughout the change for static typing.
  • hook.js: adds explicit ReactBuildType annotations — no logic changes.
  • setExtensionIconAndPopup.js: typed with ReactBuildType; copyright/Flow header added.
  • scripts/flow/react-devtools.js: adds ExtensionAction interface and runtime.getURL so chrome.action calls type-check correctly.
  • Potential issue: reduceReactBuild has no severity ordering among non-production types — if deadcode is detected first and development fires second, the display reverts to development, hiding the more severe deadcode state.

Confidence Score: 3/5

  • Safe to merge for the primary use case, but the reduction logic has an edge case where a more severe build type (e.g. deadcode) can be overwritten by a less severe one (e.g. development) if they arrive in that order.
  • The core fix (preventing a production renderer from silently overriding a previously-detected non-production build) is correct and addresses the reported issue. However, reduceReactBuild does not define severity ordering among non-production build types, meaning deadcodedevelopment sequence causes a regression in the displayed severity. The rest of the changes (typing, Flow declarations, refactoring) are clean and low-risk.
  • packages/react-devtools-extensions/src/contentScripts/reactBuildType.js — specifically the reduceReactBuild function's handling of multiple non-production build types.

Important Files Changed

Filename Overview
packages/react-devtools-extensions/src/contentScripts/reactBuildType.js New file introducing reduceReactBuild and createReactRendererListener. Core logic correctly prevents production renderers from overwriting a previously-detected non-production build, but the reduction function lacks severity ordering among non-production types (e.g. deadcode can be silently replaced by development).
packages/react-devtools-extensions/src/contentScripts/installHook.js Inline renderer event handler replaced with createReactRendererListener(window). Clean refactor with no issues.
packages/react-devtools-shared/src/backend/types.js Adds the exported ReactBuildType union type, centralising what was previously an implicit set of string literals scattered across the codebase. No issues.
packages/react-devtools-shared/src/hook.js Adds explicit ReactBuildType return-type annotation to detectReactBuildType and a type annotation to the reactBuildType variable. No logic changes; safe.
packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js Adds copyright header, Flow @flow annotation, and types parameters with ReactBuildType / number. No logic changes.
scripts/flow/react-devtools.js Adds the ExtensionAction interface (setIcon, setPopup) and runtime.getURL to the global Flow type declarations, enabling proper type-checking of chrome.action usage. No issues.

Sequence Diagram

sequenceDiagram
    participant React as React Renderer
    participant Hook as __REACT_DEVTOOLS_GLOBAL_HOOK__
    participant Listener as createReactRendererListener<br/>(installHook.js)
    participant Reduce as reduceReactBuild
    participant Window as window.postMessage
    participant BG as Background Script
    participant Icon as setExtensionIconAndPopup

    React->>Hook: renderer registered (reactBuildType)
    Hook->>Listener: emit('renderer', {reactBuildType})
    Listener->>Reduce: reduceReactBuild(displayedReactBuild, reactBuildType)
    Note over Reduce: If current is null or 'production' → return next<br/>If current is non-production and next is 'production' → keep current<br/>Otherwise → return next (last non-prod wins)
    Reduce-->>Listener: updated displayedReactBuild
    Listener->>Window: postMessage({type:'react-renderer-attached', reactBuildType: displayedReactBuild})
    Window->>BG: message event
    BG->>Icon: setExtensionIconAndPopup(reactBuildType, tabId)
    Icon->>Icon: chrome.action.setIcon(...)
    Icon->>Icon: chrome.action.setPopup(...)
Loading

Last reviewed commit: 209a41c

Comment on lines +22 to +27
// We only display the "worst" build type, so if we've already detected a non-production build,
// we ignore any future production builds. This way if a page has multiple renderers,
// and at least one of them is a non-production build, we'll display that instead of "production".
return nextReactBuildType === 'production'
? currentReactBuildType
: nextReactBuildType;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity ordering not preserved among non-production builds

The function returns nextReactBuildType whenever both the current and next types are non-production, meaning a later renderer can downgrade the displayed type from a more severe to a less severe one.

Consider this sequence:

  1. Renderer 1: deadcodedisplayedReactBuild = 'deadcode'
  2. Renderer 2: development → since current is 'deadcode' (not null, not 'production') and next is not 'production', the function returns 'development'

The deadcode type (bad DCE in a production build — arguably the worst state) is silently replaced by 'development'. A developer would then see the less alarming development icon and miss that dead code elimination had failed.

A safer approach is to encode a severity ordering and only ever move toward a "worse" build type:

const BUILD_TYPE_SEVERITY: {[ReactBuildType]: number} = {
  production: 0,
  outdated: 1,
  development: 2,
  unminified: 3,
  deadcode: 4,
};

function reduceReactBuild(
  currentReactBuildType: null | ReactBuildType,
  nextReactBuildType: ReactBuildType,
): ReactBuildType {
  if (currentReactBuildType === null) {
    return nextReactBuildType;
  }
  return BUILD_TYPE_SEVERITY[nextReactBuildType] >
    BUILD_TYPE_SEVERITY[currentReactBuildType]
    ? nextReactBuildType
    : currentReactBuildType;
}

This guarantees the displayed build type never regresses to a less severe state.

Comment on lines +36 to +48
return function ({reactBuildType}) {
displayedReactBuild = reduceReactBuild(displayedReactBuild, reactBuildType);

target.postMessage(
{
source: 'react-devtools-hook',
payload: {
type: 'react-renderer-attached',
reactBuildType: displayedReactBuild,
},
},
'*',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unnecessary postMessage when displayed build type hasn't changed

When a production renderer fires after a non-production build type has already been recorded, displayedReactBuild is unchanged (e.g., stays 'development'), but postMessage is still called. This triggers an unnecessary round-trip to the background script and redundant chrome.action.setIcon/chrome.action.setPopup calls.

Consider guarding the message with a check:

return function ({reactBuildType}) {
  const prevDisplayedReactBuild = displayedReactBuild;
  displayedReactBuild = reduceReactBuild(displayedReactBuild, reactBuildType);

  if (displayedReactBuild === prevDisplayedReactBuild) {
    return;
  }

  target.postMessage(
    {
      source: 'react-devtools-hook',
      payload: {
        type: 'react-renderer-attached',
        reactBuildType: displayedReactBuild,
      },
    },
    '*',
  );
};

This is not critical since the background script is idempotent, but it avoids unnecessary work in multi-renderer setups.

@greptile-apps

greptile-apps Bot commented Mar 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a bug where the React DevTools extension could display the production build icon/popup even when a development (or otherwise non-production) React renderer was present on the page — specifically triggered by Next.js pages that load both a production server renderer and a development client renderer.

The fix introduces a stateful listener factory (createReactRendererListener) and a reduceReactBuild reducer in the new reactBuildType.js module. The reducer ensures that once any non-production build type is detected, a subsequent production renderer cannot overwrite it. Additional changes centralize the ReactBuildType union type in types.js, add proper Flow annotations throughout, and extend the global Flow declarations with the ExtensionAction interface and runtime.getURL.

Key observations:

  • The core fix (production can't hide a non-production build) is logically correct and directly addresses the reported issue.
  • Among non-production build types (development, deadcode, unminified, outdated), the "latest wins" behavior is preserved from before — the inline comment saying "worst build type" overstates what the reducer actually enforces, which could mislead future maintainers.
  • postMessage is sent on every renderer event even when displayedReactBuild has not changed, causing the background page to redundantly re-apply the same icon/popup.
  • There are no unit tests for the new reduceReactBuild logic despite it containing the core business logic of the fix.

Confidence Score: 4/5

  • This PR is safe to merge; the fix is well-scoped and the two minor style issues do not affect correctness.
  • The primary bug fix logic is correct and well-reasoned. The only deductions are a slightly misleading comment about ordering among non-production build types, a redundant postMessage call when state is unchanged, and the absence of unit tests for the new reducer. None of these cause incorrect runtime behavior.
  • packages/react-devtools-extensions/src/contentScripts/reactBuildType.js — contains the core reduction logic with the misleading comment and the redundant postMessage; worth a second read before merging.

Important Files Changed

Filename Overview
packages/react-devtools-extensions/src/contentScripts/reactBuildType.js New file introducing reduceReactBuild and createReactRendererListener; core logic is correct but the "worst build" comment is misleading and postMessage fires redundantly when state hasn't changed.
packages/react-devtools-extensions/src/contentScripts/installHook.js Cleanly replaces the inline renderer listener with createReactRendererListener(window); straightforward and correct refactor.
packages/react-devtools-extensions/src/background/setExtensionIconAndPopup.js Adds Flow type annotation (ReactBuildType) and copyright header; no logic changes, safe.
packages/react-devtools-shared/src/backend/types.js Adds the ReactBuildType union type export, correctly enumerating all five build categories used throughout the codebase.
packages/react-devtools-shared/src/hook.js Adds ReactBuildType return type annotation to detectReactBuildType and an explicit type annotation to the local reactBuildType variable; no logic changes.
scripts/flow/react-devtools.js Adds ExtensionAction interface (covering setIcon/setPopup) and runtime.getURL to the global Flow declarations; correctly mirrors the WebExtension API shape used in setExtensionIconAndPopup.js.

Fix All in Claude Code Fix All in Codex

Last reviewed commit: 209a41c

Comment on lines +22 to +27
// We only display the "worst" build type, so if we've already detected a non-production build,
// we ignore any future production builds. This way if a page has multiple renderers,
// and at least one of them is a non-production build, we'll display that instead of "production".
return nextReactBuildType === 'production'
? currentReactBuildType
: nextReactBuildType;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment inconsistent with actual ordering logic

The comment says "We only display the 'worst' build type" which implies a total severity ordering, but among non-production builds the function simply uses the latest non-production type seen — no priority is enforced. For example, if a 'deadcode' renderer is detected first and a 'development' renderer is detected second, the displayed type silently changes from 'deadcode' to 'development', even if 'deadcode' could be considered more severe.

This matches pre-existing behavior (last-renderer-wins for same-category types) and the PR explicitly scopes "worse" to mean any non-production build vs. production, so this isn't a regression. But the comment should be made more precise to avoid misleading future readers:

Suggested change
// We only display the "worst" build type, so if we've already detected a non-production build,
// we ignore any future production builds. This way if a page has multiple renderers,
// and at least one of them is a non-production build, we'll display that instead of "production".
return nextReactBuildType === 'production'
? currentReactBuildType
: nextReactBuildType;
// We only prevent production builds from hiding previously detected non-production builds.
// Among non-production build types (development, deadcode, unminified, outdated), we use
// the latest one seen, since all of them are equally "worse" than production.
return nextReactBuildType === 'production'
? currentReactBuildType
: nextReactBuildType;
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-extensions/src/contentScripts/reactBuildType.js
Line: 22-27

Comment:
**Comment inconsistent with actual ordering logic**

The comment says *"We only display the 'worst' build type"* which implies a total severity ordering, but among non-production builds the function simply uses the **latest** non-production type seen — no priority is enforced. For example, if a `'deadcode'` renderer is detected first and a `'development'` renderer is detected second, the displayed type silently changes from `'deadcode'` to `'development'`, even if `'deadcode'` could be considered more severe.

This matches pre-existing behavior (last-renderer-wins for same-category types) and the PR explicitly scopes "worse" to mean *any non-production build vs. production*, so this isn't a regression. But the comment should be made more precise to avoid misleading future readers:

```suggestion
  // We only prevent production builds from hiding previously detected non-production builds.
  // Among non-production build types (development, deadcode, unminified, outdated), we use
  // the latest one seen, since all of them are equally "worse" than production.
  return nextReactBuildType === 'production'
    ? currentReactBuildType
    : nextReactBuildType;
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

Comment on lines +36 to +48
return function ({reactBuildType}) {
displayedReactBuild = reduceReactBuild(displayedReactBuild, reactBuildType);

target.postMessage(
{
source: 'react-devtools-hook',
payload: {
type: 'react-renderer-attached',
reactBuildType: displayedReactBuild,
},
},
'*',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

postMessage fires even when displayed build type is unchanged

target.postMessage is called on every renderer event regardless of whether displayedReactBuild actually changed. For example, if the page already shows 'development' and a second 'production' renderer fires, reduceReactBuild correctly keeps 'development', but a redundant message is still sent to the background page, which will re-apply the same icon and popup.

While harmless, this can be avoided with a simple guard:

Suggested change
return function ({reactBuildType}) {
displayedReactBuild = reduceReactBuild(displayedReactBuild, reactBuildType);
target.postMessage(
{
source: 'react-devtools-hook',
payload: {
type: 'react-renderer-attached',
reactBuildType: displayedReactBuild,
},
},
'*',
);
return function ({reactBuildType}) {
const nextBuild = reduceReactBuild(displayedReactBuild, reactBuildType);
if (nextBuild === displayedReactBuild) {
return;
}
displayedReactBuild = nextBuild;
target.postMessage(
{
source: 'react-devtools-hook',
payload: {
type: 'react-renderer-attached',
reactBuildType: displayedReactBuild,
},
},
'*',
);
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-devtools-extensions/src/contentScripts/reactBuildType.js
Line: 36-48

Comment:
**`postMessage` fires even when displayed build type is unchanged**

`target.postMessage` is called on every `renderer` event regardless of whether `displayedReactBuild` actually changed. For example, if the page already shows `'development'` and a second `'production'` renderer fires, `reduceReactBuild` correctly keeps `'development'`, but a redundant message is still sent to the background page, which will re-apply the same icon and popup.

While harmless, this can be avoided with a simple guard:

```suggestion
  return function ({reactBuildType}) {
    const nextBuild = reduceReactBuild(displayedReactBuild, reactBuildType);
    if (nextBuild === displayedReactBuild) {
      return;
    }
    displayedReactBuild = nextBuild;

    target.postMessage(
      {
        source: 'react-devtools-hook',
        payload: {
          type: 'react-renderer-attached',
          reactBuildType: displayedReactBuild,
        },
      },
      '*',
    );
  };
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

@greptile-apps-staging

Copy link
Copy Markdown

🦖 T-Rex

Generated 2 test(s) for 1 file(s): 0 passed, 2 failed, 0 skipped

Test Coverage Summary

Validated build type detection and severity ordering in the React DevTools extension, covering renderer event handling and build type aggregation logic. Two functional defects were identified: the build type reducer lacks proper severity ordering among non-production types (allowing less critical issues to overwrite more critical ones), and the event listener posts redundant messages when the effective build type hasn't changed, causing unnecessary UI updates.

Potential Issues

  • HIGH — reduceReactBuild does not replace deadcode with development (severity ordering): Tests the severity ordering bug in reduceReactBuild. The function has no ordering among non-production types — 'deadcode' (most critical) gets overwritten by 'development' when it appears later. The comment says 'worst build type' but the implementation is 'last non-production wins'.
    🛠 Fix: Implement a severity ordering for non-production build types (e.g. deadcode > development > unminified > outdated > production) and always keep the more severe type. When both current and next are non-production, return the one with higher severity rather than always returning the next one.
  • LOW — createReactRendererListener does not post duplicate messages when build type is unchanged: Tests that createReactRendererListener skips postMessage when the displayedReactBuild hasn't changed. After 'development' is seen, a subsequent 'production' renderer doesn't change the effective type but still triggers a postMessage, causing redundant icon/popup updates.
    🛠 Fix: Track the previously posted build type and skip calling postMessage when displayedReactBuild hasn't changed after the reduce operation.
Test Results (2 tests)
reduceReactBuild does not replace deadcode with development (severity ordering) HIGH packages/react-devtools-extensions/src/contentScripts/reactBuildType.js (0/2 passed)

Why this failed: Tests the severity ordering bug in reduceReactBuild. The function has no ordering among non-production types — 'deadcode' (most critical) gets overwritten by 'development' when it appears later. The comment says 'worst build type' but the implementation is 'last non-production wins'.

Terminal Output

TAP version 13
# Subtest: reduceReactBuild - severity ordering among non-production builds
    # Subtest: should keep deadcode when next is development (deadcode is more critical)
    not ok 1 - should keep deadcode when next is development (deadcode is more critical)
      ---
      duration_ms: 1.723486
      type: 'test'
      location: '/tmp/reactBuildType.test.mjs:32:3'
      failureType: 'testCodeFailure'
      error: |-
        deadcode should not be replaced by development — deadcode is more severe
        + actual - expected
        
        + 'development'
        - 'deadcode'
             ^
        
      code: 'ERR_ASSERTION'
      name: 'AssertionError'
      expected: 'deadcode'
      actual: 'development'
      operator: 'strictEqual'
      stack: |-
        TestContext.<anonymous> (file:///tmp/reactBuildType.test.mjs:37:12)
        Test.runInAsyncScope (node:async_hooks:214:14)
        Test.run (node:internal/test_runner/test:1047:25)
        Test.start (node:internal/test_runner/test:944:17)
        node:internal/test_runner/test:1440:71
        node:internal/per_context/primordials:466:82
        new Promise (<anonymous>)
        new SafePromise (node:internal/per_context/primordials:435:3)
        node:internal/per_context/primordials:466:9
        Array.map (<anonymous>)
      ...
    # Subtest: should keep deadcode when next is unminified
    not ok 2 - should keep deadcode when next is unminifie
... (truncated; showing end of output)
e (node:async_hooks:214:14)
        Test.run (node:internal/test_runner/test:1047:25)
        Suite.processPendingSubtests (node:internal/test_runner/test:744:18)
        Test.postRun (node:internal/test_runner/test:1173:19)
        Test.run (node:internal/test_runner/test:1101:12)
        async Suite.processPendingSubtests (node:internal/test_runner/test:744:7)
      ...
    # Subtest: non-production build should beat production regardless of order
    ok 4 - non-production build should beat production regardless of order
      ---
      duration_ms: 0.19573
      type: 'test'
      ...
    # Subtest: production then any non-production should show the non-production type
    ok 5 - production then any non-production should show the non-production type
      ---
      duration_ms: 0.180968
      type: 'test'
      ...
    1..5
n

Command failed: node --test /tmp/reactBuildType.test.mjs

Test Code - ../../../tmp/reactBuildType.test.mjs

/**
 * Tests for the reduceReactBuild logic in reactBuildType.js
 *
 * Issue: reduceReactBuild has no severity ordering among non-production build types.
 * When a 'deadcode' renderer is seen first, then 'development', the result
 * changes to 'development', potentially hiding a more critical issue.
 *
 * The comment says "we only display the 'worst' build type" but the implementation
 * uses "last non-production wins" instead of any actual severity ordering.
 */

import {test, describe} from 'node:test';
import assert from 'node:assert/strict';

// Extract the pure reduceReactBuild logic from reactBuildType.js (stripping Flow types)
// This mirrors the exact logic from the PR diff
function reduceReactBuild(currentReactBuildType, nextReactBuildType) {
  if (
    currentReactBuildType === null ||
    currentReactBuildType === 'production'
  ) {
    return nextReactBuildType;
  }

  // From the PR: "We only display the 'worst' build type"
  return nextReactBuildType === 'production'
    ? currentReactBuildType
    : nextReactBuildType;
}

describe('reduceReactBuild - severity ordering among non-production builds', () => {
  test('should keep deadcode when next is development (deadcode is more critical)', () => {
    // If we already detected 'deadcode' (most severe: dead code elimination failure),
    // seeing a 'development' renderer should NOT overwrite it.
    // The comment says "worst build type" — deadcode > development.
    const result = reduceReactBuild('deadcode', 'development');
    assert.equal(result, 'deadcode',
      'deadcode should not be replaced by development — deadcode is more severe');
  });

  test('should keep deadcode when next is unminified', () => {
    const result = reduceReactBuild('deadcode', 'unminified');
    assert.equal(result, 'deadcode',
      'deadcode should not be replaced by unminified');
  });

  test('should keep deadcode when next is outdated', () => {
    const result = reduceReactBuild('deadcode', 'outdated');
    assert.equal(result, 'deadcode',
      'deadcode should not be replaced by outdated');
  });

  test('non-production build should beat production regardless of order', () => {
    // Baseline: the fundamental feature of the PR
    assert.equal(reduceReactBuild('development', 'production'), 'development');
    assert.equal(reduceReactBuild('deadcode', 'production'), 'deadcode');
    assert.equal(reduceReactBuild('unminified', 'production'), 'unminified');
    assert.equal(reduceReactBuild('outdated', 'production'), 'outdated');
  });

  test('production then any non-production should show the non-production type', () => {
    assert.equal(reduceReactBuild('production', 'development'), 'development');
    assert.equal(reduceReactBuild('production', 'deadcode'), 'deadcode');
  });
});
createReactRendererListener does not post duplicate messages when build type is unchanged LOW packages/react-devtools-extensions/src/contentScripts/reactBuildType.js (0/2 passed)

Why this failed: Tests that createReactRendererListener skips postMessage when the displayedReactBuild hasn't changed. After 'development' is seen, a subsequent 'production' renderer doesn't change the effective type but still triggers a postMessage, causing redundant icon/popup updates.

Terminal Output

TAP version 13
# Subtest: createReactRendererListener - message deduplication
    # Subtest: should NOT post a new message when build type is already development and production renderer attaches
    not ok 1 - should NOT post a new message when build type is already development and production renderer attaches
      ---
      duration_ms: 1.639419
      type: 'test'
      location: '/tmp/reactBuildTypeListener.test.mjs:45:3'
      failureType: 'testCodeFailure'
      error: |-
        postMessage should NOT be called again when the displayed build type does not change (development stays after seeing production)
        
        2 !== 1
        
      code: 'ERR_ASSERTION'
      name: 'AssertionError'
      expected: 1
      actual: 2
      operator: 'strictEqual'
      stack: |-
        TestContext.<anonymous> (file:///tmp/reactBuildTypeListener.test.mjs:61:12)
        Test.runInAsyncScope (node:async_hooks:214:14)
        Test.run (node:internal/test_runner/test:1047:25)
        Test.start (node:internal/test_runner/test:944:17)
        node:internal/test_runner/test:1440:71
        node:internal/per_context/primordials:466:82
        new Promise (<anonymous>)
        new SafePromise (node:internal/per_context/primordials:435:3)
        node:internal/per_context/primordials:466:9
        Array.map (<anonymous>)
      ...
    # Subtest: should NOT post multiple identical messages for multiple production renderers aft
... (truncated; showing end of output)
:async_hooks:214:14)
        Test.run (node:internal/test_runner/test:1047:25)
        Suite.processPendingSubtests (node:internal/test_runner/test:744:18)
        Test.postRun (node:internal/test_runner/test:1173:19)
        Test.run (node:internal/test_runner/test:1101:12)
        async Promise.all (index 0)
        async Suite.run (node:internal/test_runner/test:1442:7)
        async startSubtestAfterBootstrap (node:internal/test_runner/harness:296:3)
      ...
    1..2
not ok 1 - createReactRendererListener - message deduplication
  ---
  duration_ms: 3.279611
  type: 'suite'
  location: '/tmp/reactBuildTypeListener.test.mjs:44:1'
  failureType: 'subtestsFailed'
  error: '2 subtests failed'
  code: 'ERR_TEST_FAILURE'
  ...
1..1
# tests 2
# suites 1
# pass 0
# fail 2
# cancelled 0
# skipped 0
# todo 0
# duration_ms 75.362094


Command failed: node --test /tmp/reactBuildTypeListener.test.mjs

Test Code - ../../../tmp/reactBuildTypeListener.test.mjs

/**
 * Tests for createReactRendererListener in reactBuildType.js
 *
 * Issue: listener always calls postMessage even when displayedReactBuild
 * hasn't changed (e.g. after seeing 'development', a 'production' renderer
 * doesn't change the displayed type but still fires a redundant message).
 */

import {test, describe} from 'node:test';
import assert from 'node:assert/strict';

// Replicate createReactRendererListener logic from the PR diff (without Flow types)
function reduceReactBuild(currentReactBuildType, nextReactBuildType) {
  if (
    currentReactBuildType === null ||
    currentReactBuildType === 'production'
  ) {
    return nextReactBuildType;
  }
  return nextReactBuildType === 'production'
    ? currentReactBuildType
    : nextReactBuildType;
}

function createReactRendererListener(target) {
  let displayedReactBuild = null;

  return function ({reactBuildType}) {
    displayedReactBuild = reduceReactBuild(displayedReactBuild, reactBuildType);

    target.postMessage(
      {
        source: 'react-devtools-hook',
        payload: {
          type: 'react-renderer-attached',
          reactBuildType: displayedReactBuild,
        },
      },
      '*',
    );
  };
}

describe('createReactRendererListener - message deduplication', () => {
  test('should NOT post a new message when build type is already development and production renderer attaches', () => {
    const messages = [];
    const mockTarget = {
      postMessage: (msg) => messages.push(msg),
    };

    const listener = createReactRendererListener(mockTarget);

    // First renderer: development
    listener({reactBuildType: 'development'});
    assert.equal(messages.length, 1);
    assert.equal(messages[0].payload.reactBuildType, 'development');

    // Second renderer: production — displayedReactBuild should stay 'development'
    // Since the effective value doesn't change, no new message should be sent
    listener({reactBuildType: 'production'});
    assert.equal(messages.length, 1,
      'postMessage should NOT be called again when the displayed build type does not change (development stays after seeing production)');
  });

  test('should NOT post multiple identical messages for multiple production renderers after non-production', () => {
    const messages = [];
    const mockTarget = {
      postMessage: (msg) => messages.push(msg),
    };

    const listener = createReactRendererListener(mockTarget);

    listener({reactBuildType: 'development'});
    listener({reactBuildType: 'production'});
    listener({reactBuildType: 'production'});
    listener({reactBuildType: 'production'});

    // Should only have 1 message total (for the initial 'development' detection)
    assert.equal(messages.length, 1,
      'Should not re-post the same build type on every production renderer event');
  });
});

Model: testgen | Tokens: 212,132 (201,578 in, 10,554 out) | Cost: $0.7630

Agent Logs

Generated by Greptile

@everettbu

Copy link
Copy Markdown
Author

Upstream PR was closed or merged. Code is synced via branch mirror.

@everettbu everettbu closed this Mar 11, 2026
@everettbu
everettbu deleted the sebbie/03-10-_devtools_ignore_new_production_renderers_if_we_already_use_worse_versions_of_react_on_a_page branch March 11, 2026 10:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants