Skip to content

linter: react/only-export-components false positive on non-exported nested PascalCase component #24854

Description

@golden-eng-beers

react/only-export-components flags a non-exported, nested, PascalCase function that returns JSX as if it were an exported component. Because the rule exists to protect React Fast Refresh — which is purely a property of a module's export surface — a function that is never exported cannot affect Fast Refresh, so it should not trigger the rule. The upstream reference implementation (eslint-plugin-react-refresh) does not flag it.

oxlint version

1.75.0

Config

{
  "plugins": ["typescript", "react", "oxc"],
  "rules": {
    "react/only-export-components": ["warn", { "allowConstantExport": true }],
  },
}

Minimal reproduction

// repro.tsx
import React from "react";

// The file's only export is a NON-component.
export const helper = 1;

// This PascalCase, JSX-returning function is defined inside a factory and is
// NEVER exported.
function factory() {
  return function Inner() {
    return <span />;
  };
}
void factory;

Actual behavior

repro.tsx:5:19: warning react(only-export-components): Fast refresh only works
when a file only exports components. Move your component(s) to a separate file.

The warning points at the non-exported inner function Inner.

Expected behavior

No warning. Inner is never exported, so it is invisible to Fast Refresh. The module's only export (helper) is a non-component, and a module that exports only non-components does not impede Fast Refresh.

Cross-check against the upstream rule

eslint-plugin-react-refresh (the rule oxlint mirrors) reports nothing on the exact same file.

What isolates the trigger

Two one-line changes each make the oxlint warning disappear, showing the trigger is specifically "a non-exported module-scope PascalCase function that returns JSX":

  • Rename the inner function to lowercase (inner) → no warning.

  • Export a real component from the file (so the export surface is all-components) → no warning:

    // control.tsx — oxlint clean, ESLint clean
    import React from "react";
    export function Outer() {
      return <span />;
    }
    function factory() {
      return function Inner() {
        return <span />;
      };
    }
    void factory;

Note

This looks adjacent to #22039 ("align rule with upstream cases", merged) and #21702, but the non-exported nested-component case still reproduces on 1.75.0. The real-world pattern that surfaced it is a chart module that exports a single non-component render helper and internally uses a factory returning a named Bar-shape component for Recharts.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions