Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I customize the style of a React component shared between lazy-loaded pages?

I'm building a React application and I started using CRA. I configured the routes of the app using React Router. Pages components are lazy-loaded.

There are 2 pages: Home and About.

...
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));

...
      <Suspense fallback={<div>Loading...</div>}>
        <Switch>
          <Route path="/about" component={About} />
          <Route path="/" component={Home} />
        </Switch>
      </Suspense>
...

Each page uses the Button component below.

import React from 'react';
import styles from './Button.module.scss';

const Button = ({ children, className = '' }) => (
    <button className={`${styles.btn} ${className}`}>{children}</button>
);

export default Button;

The Button.module.scss file just sets the background color of the button to red.

.btn {
    background: red;
}

The Button component accepts a className prop which is then added to the rendered button. This is because I want to give freedom to the consumer of the component. For example, in some pages margins could be needed or the background should be yellow instead of red.

To make it simple, I just want to have a different background color for the Button based on the current page, so that:

  • Home page => Blue button
  • About page => Yellow button

Each page is defined as below:

import React from 'react';
import Button from './Button';
import styles from './[PageName].module.scss';

const [PageName] = () => (
    <div>
        <h1>[PageName]</h1>
        <Button className={styles.pageBtn}>[ExpectedColor]</Button>
    </div>
);

export default [PageName];

where [PageName] is the name of the page and [ExpectedColor] is the corresponding expected color based on the above bullet list (blue or yellow).

The imported SCSS module, exports a class .pageBtn which sets the background property to the desired color.

Note: I could use a prop on the Button component which defines the variant to display (Blue/Yellow) and based on that prop add a class defined in the SCSS file. I don't want to do that since the change could be something that doesn't belong to a variant (e.g. margin-top).

The problem

If I run the application using yarn start, the application works fine. However, if I build the application (yarn build) and then I start serving the application (e.g. using serve -s build), the behavior is different and the application doesn't work as expected.

When the Home page is loaded, the button is correctly shown with a blue background. Inspecting the loaded CSS chunk, it contains:

.Button_btn__2cUFR {
    background: red
}

.Home_pageBtn__nnyWK {
    background: blue
}

That's fine. Then I click on the navigation link to open the About page. Even in this case, the button is shown correctly with a yellow background. Inspecting the loaded CSS chunk, it contains:

.Button_btn__2cUFR {
    background: red
}

.About_pageBtn__3jjV7 {
    background: yellow
}

When I go back to the Home page, the button is now displayed with a red background instead of yellow. That's because the About page has loaded the CSS above which defines again the Button_btn__2cUFR class. Since the class is now after the Home_pageBtn__nnyWK class definition, the button is displayed as red.

Note: the Button component is not exported on the common chunk because its size is too small. Having that in a common chunk could solve the problem. However, my question is about small shared components.

Solutions

I have thought to 2 solutions which, however, I don't like too much:

Increase selectors specificity

The classes specified in the [PageName].module.scss could be defined as:

.pageBtn.pageBtn {
   background: [color];
}

This will increase the selector specificity and will override the default Button_btn__2cUFR class. However, each page chunk will include the shared components in case the component is quite small (less than 30kb). Also, the consumer of the component has to know that trick.

Eject and configure webpack

Ejecting the app (or using something like react-app-rewired) would allow specifying the minimum size for common chunk using webpack. However, that's not what I would like for all the components.


To summarize, the question is: what is the correct working way of overriding styles of shared components when using lazy-loaded routes?

like image 550
Omar Muscatello Avatar asked Jul 06 '20 13:07

Omar Muscatello


People also ask

What built in React Method technique you would to lazy load components?

The React. lazy() function allows you to render a dynamic import as a normal component. It makes it simple to construct components that are loaded dynamically yet rendered as regular components.

Which React wrapper component must you use around a lazy loaded component?

The component above that we are lazy loading should then be wrapped with the React. Suspense component, this way we can have a fallback content (such as a loading indicator) while we are waiting for the dom to be painted with the lazy loaded component.


2 Answers

You can use the following logic with config file for any pages. Also, You can send config data from remote server (req/res API) and handle with redux.

See Demo: CodeSandBox

create components directory and create files like below:

src
 |---components
      |---Button
      |     |---Button.jsx
      |     |---Button.module.css

Button Component:

// Button.jsx

import React from "react";
import styles from "./Button.module.css";

const Button = props => {
  const { children, className, ...otherProps } = props;
  return (
    <button className={styles[`${className}`]} {...otherProps}>
      {children}
    </button>
  );
};

export default Button;

...

// Button.module.css

.Home_btn {
  background: red;
}
.About_btn {
  background: blue;
}

create utils directory and create AppUtils.js file:

This file handle config files of pages and return new object

class AppUtils {
  static setRoutes(config) {
    let routes = [...config.routes];

    if (config.settings) {
      routes = routes.map(route => {
        return {
          ...route,
          settings: { ...config.settings, ...route.settings }
        };
      });
    }

    return [...routes];
  }

  static generateRoutesFromConfigs(configs) {
    let allRoutes = [];
    configs.forEach(config => {
      allRoutes = [...allRoutes, ...this.setRoutes(config)];
    });
    return allRoutes;
  }
}

export default AppUtils;

create app-configs directory and create routesConfig.jsx file:

This file lists and organizes routes.

import React from "react";

import AppUtils from "../utils/AppUtils";
import { pagesConfig } from "../pages/pagesConfig";

const routeConfigs = [...pagesConfig];

const routes = [
  ...AppUtils.generateRoutesFromConfigs(routeConfigs),
  {
    component: () => <h1>404 page not found</h1>
  }
];

export default routes;

Modify index.js and App.js files to:

// index.js

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>,
  rootElement
);

...

react-router-config: Static route configuration helpers for React Router.

// App.js

import React, { Suspense } from "react";
import { Switch, Link } from "react-router-dom";
import { renderRoutes } from "react-router-config";

import routes from "./app-configs/routesConfig";

import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
      <Suspense fallback={<h1>loading....</h1>}>
        <Switch>{renderRoutes(routes)}</Switch>
      </Suspense>
    </div>
  );
}

create pages directory and create files and subdirectory like below:

src
 |---pages
      |---about
      |     |---AboutPage.jsx
      |     |---AboutPageConfig.jsx
      |
      |---home
           |---HomePage.jsx
           |---HomePageConfig.jsx
      |
      |---pagesConfig.js

About Page files:

// AboutPage.jsx

import React from "react";
import Button from "../../components/Button/Button";

const AboutPage = props => {
  const btnClass = props.route.settings.layout.config.buttonClass;
  return (
    <>
      <h1>about page</h1>
      <Button className={btnClass}>about button</Button>
    </>
  );
};

export default AboutPage;

...

// AboutPageConfig.jsx

import React from "react";

export const AboutPageConfig = {
  settings: {
    layout: {
      config: {
        buttonClass: "About_btn"
      }
    }
  },
  routes: [
    {
      path: "/about",
      exact: true,
      component: React.lazy(() => import("./AboutPage"))
    }
  ]
};

Home Page files:

// HomePage.jsx

import React from "react";
import Button from "../../components/Button/Button";

const HomePage = props => {
  const btnClass = props.route.settings.layout.config.buttonClass;
  return (
    <>
      <h1>home page</h1>
      <Button className={btnClass}>home button</Button>
    </>
  );
};

export default HomePage;

...

// HomePageConfig.jsx

import React from "react";

export const HomePageConfig = {
  settings: {
    layout: {
      config: {
        buttonClass: "Home_btn"
      }
    }
  },
  routes: [
    {
      path: "/",
      exact: true,
      component: React.lazy(() => import("./HomePage"))
    }
  ]
};

...

// pagesConfig.js

import { HomePageConfig } from "./home/HomePageConfig";
import { AboutPageConfig } from "./about/AboutPageConfig";

export const pagesConfig = [HomePageConfig, AboutPageConfig];

Edited section: With HOC Maybe this way: CodeSandBox

create hoc dir and withPage.jsx file:

src
 |---hoc
      |---withPage.jsx

...

// withPage.jsx

import React, { useEffect, useState } from "react";

export function withPage(Component, path) {
  function loadComponentFromPath(path, setStyles) {
     import(path).then(component => setStyles(component.default));
   }
  return function(props) {
    const [styles, setStyles] = useState();
    
    useEffect(() => {
      loadComponentFromPath(`../pages/${path}`, setStyles);
    }, []);
    return <Component {...props} styles={styles} />;
  };
}

And then pages like below:

src
 |---pages
      |---about
      |     |---About.jsx
      |     |---About.module.css
      |
      |---home
           |---Home.jsx
           |---Home.module.css

About.jsx file:

// About.jsx

import React from "react";
import { withPage } from "../../hoc/withPage";

const About = props => {
  const {styles} = props;
  return (
    <button className={styles && styles.AboutBtn}>About</button>
  );
};

export default withPage(About, "about/About.module.css");

About.module.css file:

// About.module.css

.AboutBtn {
  background: yellow;
}

Home.jsx file:

// Home.jsx

import React from "react";
import { withPage } from "../../hoc/withPage";

const Home = props => {
  const { styles } = props;
  return <button className={styles && styles.HomeBtn}>Home</button>;
};

export default withPage(Home, "home/Home.module.css");

Home.module.css file:

// Home.module.css

.HomeBtn {
  background: red;
}
like image 182
Mohammad Oftadeh Avatar answered Oct 05 '22 11:10

Mohammad Oftadeh


I would suggest instead of adding both the default styles and the consumer styles, use the consumer's styles over yours and use your as a callback if not supplied. The consumer can still compose your defaults with the composes keyword.

Button.js

import React from 'react';
import styles from './Button.module.scss';

const Button = ({ children, className}) => (
    <button className={className ?? styles.btn}>{children}</button>
);

export default Button;

SomePage.module.scss

.pageBtn {
  // First some defaults
  composes: btn from './Button.module.scss';
  // And override some of the defautls here
  background: yellow;
}

If you wish, use sass @extends or @mixin instead

EDIT: Haven't tested it, but could it be that just by using composes webpack will make sure to bundle the defaults only once? Thus you're no longer needed to change your Button.js code with the ??

like image 26
Mordechai Avatar answered Oct 05 '22 13:10

Mordechai