Previous solution was great but not perfect. We still add a CSS of two brands in one CSS file and loading all CSS for all brands per each component.
Could we do it better and load only the necessary CSS for each component for the particular branch ? Yers we could. The idea about bundle splitting is that one CSS file will be created per each component.
So in order to have one CSS file per brand we have to
Create a wrapper component per each brand.
Let’s start with
The Home component.
We are going to move the component’s code from index.js to a new file called renderer.js and repurpose index.js to be a wrapper component, which will load brand specific sub component, that will pass the style object back to the renderer component in renderer.js. If it sounds a bit confusing don’t worry. Follow that tutorial and it will get clear.
./src/components/Home/renderer.js
import React from 'react'; const Renderer = ({styles, title}) => { return ( <div> <div className={styles.wrapper}>{title}</div> </div> ); } export default Renderer;
The code in renderer file is pretty similar to that we had in index.js with one exception: we are going to pass the css as a property to this component.
And just to demonstrate how we could render different layout per each brand we are going to pass the title property as well.
Now the index.js will become our Home component wrapper, which will dynamically load either ./brands/one
or ./brands/two
sub component, which on the other hand will load our ./renderer.js
component, passing the appropriate CSS for the selected brand.
./src/components/Home/index.js
import React from 'react'; import Loadable from 'react-loadable'; import Loading from '../Loading'; const one = Loadable({ loader: () => import ('./brands/one'), loading: Loading }); const two = Loadable({ loader: () => import ('./brands/two'), loading: Loading }); const components = { one, two } const Home = ( {subDomain} ) => { const Component = components[subDomain]; return ( <Component /> ) } export default Home;
what we just did:
– we crated a wrapper component, that will conditionally load the helper component for the current brand (lines 5 and 10)
-we render the appropriate sub component, based on the brand name.
Let’s create the helper sub components that will load the brand specific CSS and pass it to the renderer component and render it.
These components will look quite similar:
./src/components/Home/brands/one/index.js
import React from 'react'; import styles from './styles.scss'; import Renderer from '../../renderer.js' export default () => { return ( <Renderer styles={styles} title="This is my home section rendered for One!" /> ) }
./src/components/Home/brands/two/index.js
import React from 'react'; import styles from './styles.scss'; import Renderer from '../../renderer.js' export default () => { return ( <Renderer styles={styles} title="This is my home section rendered for Two!" /> ) }
what we just did:
– we imported brand specific CSS in each of the components (line 2)
– imported the renderer component (line 3)
– rendered the renderer component, passing the CSS and the title property (line 7)
Open the ./dist
folder and look at 1.css and 2.css contents:
./dist/1.css
.one-wrapper{background-image:url(/dist/images/b5c0108b6972494511e73ad626d1852f-home.png);height:500px}.one-wrapper h2{color:#000}
./dist/2.css
.two-wrapper{background-image:url(/dist/images/a005b97826d5568577273d214dd5f89a-home.png);height:800px}.two-wrapper h2{color:#00f;font-size:50px}
Webpack created two files with the corresponding CSS: one-wrapper
and two-wrapper
containing only the CSS needed for each brand.
Open the browser and give it a try. The result should be what we saw in the previous chapter, but this time only the brand specific CSS is loaded.
Nice! Now we have Webpack created these two CSS files, but the class names are one-wrapper
and two-wrapper
which comes from the lead folder name, which now instead of been ./Home
is ./Home/one
and /Home/two
What will happen if we want to make another component brand specific?
The Greetings component
Let’s do the same changes:
./src/components/Greetings/renderer.js
import React, { Component } from 'react'; import { connect } from 'react-redux'; const CHANGE_USERNAME = 'CHANGE_USERNAME'; const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'; class Greetings extends Component { constructor(props) { super(props); } doneEditUsername() { let newName = document.querySelector('#inputField').value; this.props.changeUserName(newName); this.props.toggleLogInPopup(); } usernameChanged(el) { let newName = el.target.value; this.props.changeUserName(newName); } onToggleEditMode() { this.props.toggleLogInPopup(); } render() { let element = <h2 onClick={() =>{ this.onToggleEditMode() }}>Hello: {this.props.userName}</h2>; if(this.props.editMode) element = <h2>Type new name:<input type="text" id='inputField' value={this.props.userName} onChange={(el) => { this.usernameChanged(el);}} /><button onClick={() =>{ this.doneEditUsername() }}>done</button></h2> return ( <div> <div className={this.props.styles.wrapper}> {element} </div> </div>); } } const mapStateToProps = ( storeState ) => { return { userName: storeState.user.userName, editMode: storeState.user.editMode } } const mapDispatchToProps = dispatch => { return { toggleLogInPopup: () => { dispatch({type: TOGGLE_EDIT_MODE}); }, changeUserName: (userName) => { dispatch({type: CHANGE_USERNAME, data: userName}); } } }; export default connect( mapStateToProps, mapDispatchToProps )(Greetings);
what we just did:
– we just moved the code from ./index.js to ./renderer.js
– we removed the CSS import since we are passing CSS as a property
– we changed the div class name to come from the passed property (line 36)
The rest is the same like in Home component.
The index component will actually look exactly the same:
./src/components/Greetings/index.js
import React from 'react'; import Loadable from 'react-loadable'; import Loading from '../Loading'; const one = Loadable({ loader: () => import ('./brands/one'), loading: Loading }); const two = Loadable({ loader: () => import ('./brands/two'), loading: Loading }); const components = { one, two } const Home = ( {subDomain} ) => { const Component = components[subDomain]; return ( <Component /> ) } export default Home;
Let’s load different ‘home’ pictures for each brand.
Move the home picture in ./images/home.png
to ./images/one/home.png
and add another image for ./images/two/home.png
(you could either download some png or use the one in this branch)
./src/components/Greetings/brands/one/styles.scss
.wrapper { background-image:url('../../../../images/one/home.png'); height: 500px; h2 { color: black; } }
./src/components/Greetings/brands/two/styles.scss
.wrapper { background-image:url('../../../../images/two/home.png'); height: 800px; h2 { color: blue; font-size: 50px; } }
Here we have to adjust the relative path to the images since this component goes two levels deeper and we moved the images into a brands folders (line 2)
And the helper sub components are the same like in Home component.
./src/components/Greetings/brands/one/index.js
import React from 'react'; import styles from './styles.scss'; import Renderer from '../../renderer.js' const One = () => { return ( <Renderer styles={styles} /> ) } export default One;
./src/components/Greetings/brands/two/index.js
import React from 'react'; import styles from './styles.scss'; import Renderer from '../../renderer.js' const One = () => { return ( <Renderer styles={styles} /> ) } export default One;
Start the server and go to http://one.localhost:3006/home and you will notice that the Home component height increased. Why this happened?
Let’s open http://one.localhost:3006/dist/1.css and look at the class names:
.one-wrapper{background-image:url(/dist/images/b5c0108b6972494511e73ad626d1852f-home.png);height:500px}.one-wrapper h2{color:#000}
Somehow the one-wrapper has background-image:url(/dist/images/b5c0108b6972494511e73ad626d1852f-home.png)
and height:500px
that belongs to the Greetings component.
Why this is happening? Because of the way how we set up class name structure in Css-Loader. If you look at webpack.base.config.js you will see that the localIdentName
which is the CSS className
is constructed by adding the folder name, and the actual local identifier ‘[folder]-[local]’
./src/webpack.base.config.js
... // SCSS { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, importLoaders: 2, localIdentName: '[folder]-[local]', sourceMap: true } }, ...
But the folder name now is ‘one’ or ‘two’ for both components since it takes only the leaf folder name. Let’s fix this by
Make brand component names unique.
Go to src/Home/brands/one
and rename it to src/Home/brands/OneHome
and src/Home/brands/two
to be src/Home/brands/TwoHome
and do the same for the greetings component: src/Greetings/brands/one
=> src/Greetings/brands/OneGreetings
and src/Greetings/brands/Two
=> src/Greetings/brands/TwoGreetings
Next let’s make the appropriate changes in both: Home and Greeting component:
./src/Home/index.js
import React from 'react'; import Loadable from 'react-loadable'; import Loading from '../Loading'; const one = Loadable({ loader: () => import ('./brands/OneHome'), loading: Loading }); const two = Loadable({ loader: () => import ('./brands/TwoHome'), loading: Loading }); const components = { one, two } const Home = ( {subDomain} ) => { const Component = components[subDomain]; return ( <Component /> ) } export default Home;
and
./src/Greetings/index.js
import React from 'react'; import Loadable from 'react-loadable'; import Loading from '../Loading'; const one = Loadable({ loader: () => import ('./brands/OneGreetings'), loading: Loading }); const two = Loadable({ loader: () => import ('./brands/TwoGreetings'), loading: Loading }); const components = { one, two } const Home = ( {subDomain} ) => { const Component = components[subDomain]; return ( <Component /> ) } export default Home;
Run the project and check /dist/3.css
.OneHome-wrapper--t4U5b{background:#8d8dac;color:#fff;text-align:center;font-family:MyFont}
it contains only CSS for the Home component.
Adding a hash in CSS class names
As an extra step we could also add a hash for each class name. This will make class names unique per component, so if accidentally happened to have two components with the same names their CSS won’t colide.
This could be achieved by simply adding a hash
directive in localIdentName
in CSS-loader config [folder]-[local]–[hash:base64:5]
const getEnvironmentConstants = require('./getEnvironmentConstants'); const webpack =require('webpack'); module.exports = { mode: 'development', devtool: 'eval-source-map', entry: [ '@babel/polyfill', './src/index.js', ], output: { filename: '[name]-bundle.js', publicPath: '/dist/', }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader" } }, // SCSS { test: /\.scss$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true, importLoaders: 2, localIdentName: '[folder]-[local]--[hash:base64:5]', sourceMap: true } }, { loader: 'postcss-loader', options: { plugins: () => [require('autoprefixer')()], sourceMap: true }, }, { loader: 'sass-loader', options: { outputStyle: 'expanded', sourceMap: true } } ], }, // images { test: /\.(png|jp(e*)g|svg)$/, use: [{ loader: 'url-loader', options: { limit: 8000, // Convert images < 8kb to base64 strings name: 'images/[hash]-[name].[ext]' } }] }, //File loader used to load fonts { test: /\.(woff|woff2|eot|ttf|otf)$/, use: ['file-loader'] } ] }, plugins: [ new webpack.DefinePlugin({ 'process.env' : getEnvironmentConstants() } ), ] };
Run the project again and the home component should look like before. Open http://one.localhost:3006/dist/1.css and you will see how the has was appended to the class names.