Skip to content

feat: add fastRefresh option to disable Fast Refresh - #1467

Open
kklem0 wants to merge 1 commit into
vitejs:mainfrom
kklem0:feat/fast-refresh-option
Open

kklem0 wants to merge 1 commit into
vitejs:mainfrom
kklem0:feat/fast-refresh-option

Conversation

@kklem0

@kklem0 kklem0 commented Sep 21, 2026 •

Copy link
Copy Markdown

Description

Alternative to #1436, following @ArnaudBarre's suggestion in #1436 (comment): instead of exporting a second plugin, add a fastRefresh option to react() so that it can run next to a plugin that already provides Fast Refresh.

The motivating case is React Router framework mode. reactRouter() runs react-refresh/babel itself and imports its own refresh runtime into route modules, so with react({ compiler: true }) next to it a route module imports RefreshRuntime twice and fails with Identifier 'RefreshRuntime' has already been declared (remix-run/react-router#12870). That leaves no way to use the Rust React Compiler in those apps.

import { reactRouter } from '@react-router/dev/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react({ compiler: true, fastRefresh: false }), reactRouter()],
})

Behavior

  • fastRefresh: false folds into the existing skipFastRefresh logic (the same path as build, production and server.hmr: false), so the oxc jsx.refresh transform, the refresh wrapper, the preamble and the compiler's refresh flag are all skipped. HMR itself stays on.
  • JSX, TypeScript and the React Compiler stay in this plugin. vite:react-compiler is enforce: 'pre', so it sees route modules before React Router rewrites them, and JSX is lowered in the same pass as the compiler, so jsxDEV source positions match the source file (the jsx: 'preserve' concern from feat: add standalone reactCompiler plugin #1436 does not apply).
  • fastRefresh existed until 3.x and was removed in 4.0 (feat!: allow to run Babel on non js/ts extensions #122) because the plugin could always enable it on its own. This brings it back for the case where another plugin owns it.

Tests

playground/react-router is a React Router 8.5 framework-mode app (SSR) with react({ compiler: true, fastRefresh: false }) next to reactRouter():

  • serve and build: the page hydrates and the counter works, with no browser errors
  • serve: the route module is compiled, uses jsxDEV, imports RefreshRuntime once (from React Router's HMR runtime) and never references /@react-refresh
  • serve: editing the route keeps the state (count is 1 becomes count is 1!), so React Router's Fast Refresh works on the compiled output
  • build: the client bundle contains the compiled output

reactRouter() throws when Vite runs without a config file, and the default test serve uses configFile: false, so the playground has a small __tests__/serve.ts that passes its config file explicitly.

Docs: a README section for the option, a pointer from the Rust React Compiler section, and a changelog entry.

🤖 Generated with Claude Code

@ArnaudBarre

Copy link
Copy Markdown
Member

Can you update the added playground to use react-router and remove the unit tests?
Then I will check it out but the amount of changes in the source code is really low so I'm ok with going forward with this

Allows `react()` to run next to a plugin that already provides Fast
Refresh, such as React Router in framework mode, so that JSX and the
React Compiler can still be handled by this plugin.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@kklem0

kklem0 commented Oct 21, 2026

Copy link
Copy Markdown
Author

Thanks! Done: the playground is now a React Router framework-mode app (playground/react-router, SSR, with react({ compiler: true, fastRefresh: false }) next to reactRouter()), the unit tests are removed, and the branch is rebased on main.

The playground checks that the route module is compiled and only imports React Router's refresh runtime, and that editing it keeps the state.

While testing on main I noticed that the compiler's refresh condition from #1516 is inverted: without refreshInclude / refreshExclude, refresh is only enabled for non-JSX files, so components get no Fast Refresh and .ts hooks get $RefreshSig$ again (#1478). It also hides the React Router conflict on main right now. I opened #1524 to fix it.

This branch has not been deployed

No deployments
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.

2 participants