Skip to content

Conversation

uozer7050
Copy link

Fix: Preserve CSS Styles for Lazy-Loaded Components

Description:

This pull request addresses an issue where lazy-loaded components in React Router applications would lose their styles upon route changes. Previously, when navigating away from a route, or when a component was lazy-loaded and remained active in another route, its associated CSS chunks could be removed from the DOM. This resulted in the component losing its styling and appearing visually broken.

Specifically, this change resolves the scenario where a lazy-loaded component, sharing the same CSS chunk as a route, would lose its styles when navigating away from that route, even if the component itself still existed in the DOM. Before this change, the keyedLinks mechanism generated route-specific link tags, and when a route unmounted, these styles were removed, leading to the unstyled lazy-loaded component.

With this modification, the href of a CSS or JavaScript link is now tracked in a persistent Set called persistentHrefs. If a link tag's href has already been injected into the DOM (i.e., it exists in persistentHrefs), that link will not be rendered again. This approach ensures that lazy-loaded CSS continues to be available for components across route changes, preventing style loss.

Benefits of this Change:

  • Improved User Experience: Lazy-loaded components retain their styles across route transitions, providing a seamless visual experience.
  • More Robust Applications: Reduces bugs related to unexpected CSS disappearance.
  • Potential Performance Improvement: Minimizes unnecessary additions and removals of link tags.

Before/After Scenario:

  • Before: Route A imports a lazy-loaded Component X, and the Layout component of both Route A & B also imports Component X in a lazy way. When navigating from Route A to Route B, Component X would lose its styles.
  • After: In the same scenario, Component X's styles are preserved in Route B, and Component X is rendered correctly.

This change is particularly important for large and modular React applications, where dynamic loading of components and styles is a common pattern.

Illustrative Scenario:

Consider the following common setup in a React Router application:

// components/SharedComponent.tsx
// This component imports its own CSS module, e.g., via CSS Modules, Tailwind JIT, etc.
import styles from './SharedComponent.module.css';

export default function SharedComponent() {
  return <div className={styles.container}>I am a shared component!</div>;
}

// routes/RouteA.tsx
import { lazy } from 'react';
const SharedComponent = lazy(() => import('../components/SharedComponent'));

export default function RouteA() {
  return (
    <div>
      <h1>Route A</h1>
      <SharedComponent /> {/* This instance causes SharedComponent.module.css to load */}
    </div>
  );
}

// routes/RouteB.tsx
import { lazy } from 'react';
// Imagine SharedComponent is also used in RouteB, perhaps in a persistent layout,
// or as part of RouteB's content itself.
const SharedComponent = lazy(() => import('../components/SharedComponent'));

export default function RouteB() {
  return (
    <div>
      <h1>Route B</h1>
      <SharedComponent /> {/* This instance now relies on the *persistent* CSS */}
    </div>
  );
}

Before this change:

  1. Navigate to /route-a. RouteA mounts, SharedComponent is rendered, and its SharedComponent.module.css is dynamically injected into the DOM as a <link> tag.
  2. Navigate to /route-b. RouteA unmounts. The <link> tag for SharedComponent.module.css (which was implicitly associated with RouteA's lifecycle) is removed from the DOM.
  3. RouteB mounts and renders SharedComponent. Due to previous href processing and potential browser/framework deduplication, a new <link> tag for SharedComponent.module.css is not re-injected.
  4. Result: SharedComponent on RouteB appears unstyled, leading to a broken UI.

After this change:

  1. Navigate to /route-a. RouteA mounts, SharedComponent is rendered, and its SharedComponent.module.css is injected into the DOM as a <link> tag. The href for SharedComponent.module.css is added to the persistentHrefs Set.
  2. Navigate to /route-b. RouteA unmounts. The <link> tag for SharedComponent.module.css remains in the DOM because its href is now persistently tracked and not subject to removal by the Links component's filtering logic.
  3. RouteB mounts and renders SharedComponent. The Links component processes SharedComponent.module.css, but because its href is already present in persistentHrefs, it skips re-injecting a duplicate <link> tag.
  4. Result: SharedComponent on RouteB retains its styles because the original <link> tag continues to persist in the document.

This ensures that once a CSS module for a shared or lazy-loaded component is loaded, its styles remain active throughout the application's single-page application (SPA) lifecycle, regardless of dynamic route changes.

Related Issues/Discussions:

Copy link

changeset-bot bot commented Oct 3, 2025

⚠️ No Changeset found

Latest commit: 87db7c6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Oct 3, 2025

Hi @uozer7050,

Welcome, and thank you for contributing to React Router!

Before we consider your pull request, we ask that you sign our Contributor License Agreement (CLA). We require this only once.

You may review the CLA and sign it by adding your name to contributors.yml.

Once the CLA is signed, the CLA Signed label will be added to the pull request.

If you have already signed the CLA and received this response in error, or if you have any questions, please contact us at [email protected].

Thanks!

- The Remix team

@remix-cla-bot
Copy link
Contributor

remix-cla-bot bot commented Oct 3, 2025

Thank you for signing the Contributor License Agreement. Let's get this merged! 🥳

*/
// Persistent set of hrefs so that CSS/JS links are not removed when
// routes unmount. This ensures lazy-loaded CSS persists across route changes.
const persistentHrefs = new Set<string>();
Copy link

Choose a reason for hiding this comment

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

Could this be WeakSet<object>?

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

Successfully merging this pull request may close these issues.

3 participants