Skip to content

CSS Module HMR leaves server-rendered React components with stale class names when also used by a hydrated island #18105

Description

@forberg

Astro Info

- Astro: `7.3.3`
  - `@astrojs/react`: `6.0.6`
  - React / React DOM: `19.2.6`
  - Vite, resolved by Astro: `8.2.2`
  - Node.js: `24.11.0`
  - OS: Linux x64
  - Browser: Chromium, tested using Playwright

If this issue only occurs in one browser, which browser is a problem?

No response

Describe the Bug

When a React component using a CSS Module is rendered both without hydration and inside a hydrated island, editing the CSS updates the hydrated instance but leaves t he server-rendered instance with stale class names.

The stylesheet is updated successfully, but its generated class names change. The non-hydrated HTML still references the previous class names, so it loses its stylin
g. Manually refreshing the page fixes it.

This was reproduced in a minimal Astro project without custom PostCSS plugins or application-specific integrations.

Actual behavior

Stage Non-hydrated card Hydrated card
Initial load Red background Red background
After CSS edit Old class name; transparent background New class name; blue background
After manual refresh New class name; blue background New class name; blue background

The browser receives a js-update for Card.tsx, and Vite logs:

[vite] hot updated: /src/components/Card.tsx

No full page reload occurs.

Inspecting the DOM confirms that only the hydrated card receives the updated CSS Module class name. The stylesheet contains the new selector but no longer contains the selector referenced by the non-hydrated card.

Possible cause

The default CSS Module scoped-name generator incorporates stylesheet contents, so even a declaration-only edit changes the exported class names.

In the installed version, astro:hmr-reload appears to skip the server-side full reload when the stylesheet also exists in the client module graph. Client HMR updates the hydrated instance, but cannot update the non-hydrated HTML.

The presence of a stylesheet in the client graph does not necessarily mean all HTML using its exported class names is client-managed.

Workaround

Using stable CSS Module names in development fixes ordinary declaration edits. For example, in astro.config.mjs, keep production defaults and configure stable names for development:

import { defineConfig } from "astro/config";
import react from "@astrojs/react";

export default defineConfig({
  integrations: [react()],
  vite: {
    css: {
      modules: import.meta.env.DEV
        ? {
            generateScopedName: "[name]__[local]__[hash:base64:5]",
          }
        : undefined,
    },
  },
});

With this scoped-name pattern, both cards updated correctly without a reload in the same reproduction.

This is not a complete fix: changes to exported class mappings, such as class renames or composition changes, can still require refreshing the server-rendered HTML.

What's the expected result?

Both cards should reflect the updated CSS.

If updating the non-hydrated instance requires regenerating its HTML, a full page reload would also be an acceptable fallback.

Link to Minimal Reproducible Example

https://github.com/forberg/astro-hmr-bug

Participation

  • I am willing to submit a pull request for this issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P3: minor bugAn edge case that only affects very specific usage (priority)pkg: astroRelated to the core `astro` package (scope)triage: failedTriage failed unexpectedly and can be retried

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions