Table of Contents
Let’s make the task more challenging and assume that we are going to have two different Brands (or apps)
one.localhost.com,
two.localhost.com,
for the most of the cases they will do the same thing but the styling will be different, so let’s add ability for our components to be styled in different ways, depending of the brand.
Passing the sub-domain to the PageLayout component
Let’s add new parameter to store the default brand.
./.env
APP_NAME=Webpack React Tutorial GRAPHQL_URL=http://localhost:4001/graphql PROD_SERVER_PORT=3006 DEFAULT_BRAND=one
and make it available to the front end.
./getEnvironmentConstants.js
... const frontendConstants = [ 'APP_NAME', 'GRAPHQL_URL', 'PROD_SERVER_PORT', 'DEFAULT_BRAND' ]; ...
Now, let’s pass the sub domain to the PageComponent. We have to do this in two different places: one for the client side, and one on the SSR.
./components/App/index.js
import React from 'react'; import PageLayout from '../../containers/PageLayout'; import { ApolloProvider } from 'react-apollo'; import { ApolloClient } from 'apollo-client'; import { HttpLink } from 'apollo-link-http'; import { InMemoryCache } from 'apollo-cache-inmemory'; import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import { Provider } from 'react-redux'; import { createStore} from 'redux'; import reducers from '../../reducers'; const styles = require('./styles.scss'); const store = createStore(reducers, {}); export default () => { const GRAPHQL_URL = process.env.GRAPHQL_URL; const client = new ApolloClient({ link: new HttpLink({ uri: GRAPHQL_URL }), cache: new InMemoryCache().restore(window.__APOLLO_STATE__), }); const subDomain = window.location.host.split('.').length == 1 ? process.env.DEFAULT_BRAND : window.location.host.split('.')[0]; return ( <div className={styles.appWrapper}> <Provider store={store}> <ApolloProvider client={client}> <Router> <Switch> <Route exact path="*" render={(props) => <PageLayout {...props} subDomain={subDomain} />} /> </Switch> </Router> </ApolloProvider> </Provider> </div> ); }
what we just did:
– (line 23) getting the sub domain
– (line 30) passing extra parameter with the subDomain to the PageComponent
Do the same for the SSR
./components/App/ssr-index.js
import React from 'react'; import PageLayout from '../../containers/PageLayout'; import { StaticRouter, Route, Switch } from 'react-router-dom'; import { ApolloProvider } from 'react-apollo'; import { Provider } from 'react-redux'; import { createStore} from 'redux'; import reducers from '../../reducers'; import fetch from 'isomorphic-fetch'; import styles from './styles.scss'; const store = createStore(reducers, {}); export default ( {req, client} ) => { const subDomain = req.headers.host.split('.').length == 1 ? process.env.DEFAULT_BRAND : req.headers.host.split('.')[0]; const context = {}; return ( <div className={styles.appWrapper}> <Provider store={store}> <ApolloProvider client={client}> <StaticRouter location={ req.url } context={context}> <Switch> <Route exact path="*" render={(props) => <PageLayout {...props} subDomain={subDomain} />} /> </Switch> </StaticRouter> </ApolloProvider> </Provider> </div> ); }
what we just did:
– (line 23) getting the sub domain, this time from the express request object, since this is happening on the server side.
– (line 30) passing extra parameter with the subDomain to the PageComponent
Now this will work fine for the production build, but if you run yarn start-api
it will break with Invalid Host header
message. This is a security feature and we need to tell Webpack dev server that we are going to have sub-domains.
This could be done by passing disableHostCheck: true
and since we are using WebpackDevServer only for development this should be safe enough.
./server-api.js
import WebpackDevServer from 'webpack-dev-server'; import webpack from 'webpack'; import config from './webpack.api.config.js'; require('dotenv').config(); console.log(">>>" + process.env.GRAPHQL_URL); const compiler = webpack(config); const server = new WebpackDevServer(compiler, { hot: true, publicPath: config.output.publicPath, historyApiFallback: true, disableHostCheck: true, }); server.listen(8080, 'localhost', function() {});
For the CLI config, if will look like this: --disable-host-check
./package.json
... "start-cli": "webpack-dev-server --disable-host-check --hot --history-api-fallback --config webpack.cli.config.js", ...
Passing brand name to the components
This could be achieved in many different ways:
– we could either use redux and add the brand property there, exposing it to all connected components (which we will do later in this tutorial)
– or we could simply pass it as a property from the PageLayout component.
Let’s do the second one since it is straight forward.
./containers/PageLayout/index.js
import React, { Component } from 'react'; import ComponentList from './ComponentList'; import Loading from '../../components/Loading'; import query from './query'; import { graphql } from 'react-apollo'; const styles = require('./styles.scss'); class PageLayout extends Component { constructor(props) { super(props); } render() { if(!this.props.data.getPageByUrl) { return (<Loading />); } const subDomain = this.props.subDomain; const allLayout = this.props.data.getPageByUrl.layout.map((layoutList) => { const layout = layoutList.components.map((component, id , components) => { const componentName = component.name; const ChildComponent = ComponentList[componentName]; if(typeof ChildComponent === 'undefined') { return( <div key='{id}' className={styles.error}>Can't find {componentName} component!</div> ); } return ( <ChildComponent key={componentName} subDomain={subDomain} /> ); }); return layout; }); return( <div className={styles.app}> {allLayout} </div> ); } } export default graphql(query, { options(props) { return { variables: { url: props.history.location.pathname }, }; }, })(PageLayout);
Adding brand specific styling to the component
Coming up with a good naming convention and folder structure is very important, but I will leave this to you to research.
I would propose the simplest one:
component
|- brands
| |_ one
| |- styles.scss
| |_ two
| |- styles.scss
|- index.js
./components/Home/index.js
import React from 'react'; const Home = ( {subDomain} ) => { const styles = require(`./brands/${subDomain}/styles.scss`); return ( <div> <div className={styles.wrapper}>This is my home section!</div> </div> ) } export default Home;
yarn start
hit one.localhost:3006/home and then two.localhost:3006/home and you will notice the difference right away!
But how this was achieved without re-rendering the component?
Let’s look at component’s css file (it might be 1.css or different number but look at the net tab and make sure that the CSS contains something like this):
.one-wrapper{background:#8d8dac;color:#fff}.one-wrapper,.two-wrapper{text-align:center;font-family:MyFont}.two-wrapper{background:#fff;color:#000}
– first brand has .one-wrapper{background:#8d8dac;color:#fff}
and the second has two-wrapper{background:#fff;color:#000}
and all CSS that is in common is added like this: .one-wrapper,.two-wrapper{text-align:center;font-family:MyFont}
so no repeating CSS if it’s the same between brands.
Basically now the css file for this component will have all styling for both brands.
This is nice but not perfect. We still load unnecessarily the styles for the brands that we are not in to. Let’s see how to fix this in the next chapter.