Full isomorphic create-react-app + SSR + npm packed shared components with CSS
In this article we create ejected full isomorphic create-react-app
based on universal-router
, isomorphic-style-loader
with possibility import components from npm packages with CSS injection.
Create application
create-react-app create-react-app-ssr
cd create-react-app-ssr
yarn run eject
Install presets and plugin
yarn add babel-preset-latest babel-preset-react babel-preset-stage-2 babel-plugin-transform-runtime
And create .babelrc
{
"presets": [
"latest",
"react",
"stage-2"
],
"plugins": [
[
"transform-runtime", {
"polyfill": false,
"regenerator": true
}
]
]
}
Install isomorphic-style-loader
and universal-router
yarn add isomorphic-style-loader universal-router
Modify webpack
configs
webpack.config.dev.js
...
{
test: /\.css$/,
loader: `isomorphic-style-loader!css?importLoaders=1&modules&localIdentName=[name]__[local]___[hash:base64:5]!postcss`
},
...
new webpack.DefinePlugin(env.stringified),
new webpack.DefinePlugin({
'process.env.BROWSER': true,
}),
new webpack.HotModuleReplacementPlugin(),
...
webpack.config.prod.js
...
{
test: /\.css$/,
loader: `isomorphic-style-loader!css?importLoaders=1&modules&localIdentName=[name]__[local]___[hash:base64:5]!postcss`
},
...
new webpack.DefinePlugin(env.stringified),
new webpack.DefinePlugin({
'process.env.BROWSER': true,
}),
new webpack.HotModuleReplacementPlugin(),
...
Remove all from ./src
and create next folder structure
./src
./src/node_modules
./src/node_modules/components
./src/node_modules/core
./src/node_modules/pages
./src/node_modules/server
For demo app we need some extra libraries
yarn add semantic-ui-react prop-types query-string
Lets create App component in ./src/node_modules/components/App
App.js
import PropTypes from 'prop-types'
import React from 'react'
const ContextType = {
insertCss: PropTypes.func.isRequired
}
class App extends React.PureComponent {
static propTypes = {
context: PropTypes.shape(ContextType).isRequired,
children: PropTypes.element.isRequired,
}
static childContextTypes = ContextType
getChildContext() {
return this.props.context
}
render() {
return React.Children.only(this.props.children)
}
}
export default App
App.css
@import url('//cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.2/semantic.min.css');
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
package.json
{
"private": true,
"name": "App",
"main": "./App.js"
}
Create history management module in ./src/node_modules/core/history
history.js
import createBrowserHistory from 'history/createBrowserHistory'
export default process.env.BROWSER && createBrowserHistory()
go.js
import history from './history'
export default (location) => {
history.push(location)
}
package.json
{
"private": true,
"name": "history",
"main": "./history.js"
}
Some pages in ./src/node_modules/pages
Root/Root.js
import React from 'react'
import { Flex } from 'mind-flex'
import { Button } from 'semantic-ui-react'
import { Header } from 'semantic-ui-react'
import { Segment } from 'semantic-ui-react'
import go from 'core/history/go'
import s from './Root.css'
const Root = ({ title }) => (
<div className={ s.root }>
<Segment>
<Header>
{ title }
</Header>
<Flex center middle>
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</Flex>
</Segment>
<Button onClick={ () => go(`/login`) }>Login</Button>
<Button onClick={ () => go(`/something`) }>NotFound</Button>
</div>
)
import withStyles from 'isomorphic-style-loader/lib/withStyles'
export default withStyles(s)(Root)
Root/Root.css
@import "components/App/App.css";
.root {
margin: 5px;
}
Root/route.js
import React from 'react'
import Root from './Root'
const title = `Root`
export default {
path: '/',
action() {
return {
title,
component: <Root title={ title } />,
}
},
}
Login/Login.js
import React from 'react'
import { Button } from 'semantic-ui-react'
import { Header } from 'semantic-ui-react'
import { Segment } from 'semantic-ui-react'
import go from 'core/history/go'
import s from './Login.css'
const Login = ({ title }) => (
<div className={ s.root }>
<Segment>
<Header>
{ title }
</Header>
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</Segment>
<Button onClick={ () => go(`/`) }>Home</Button>
<Button onClick={ () => go(`/something`) }>NotFound</Button>
</div>
)
import withStyles from 'isomorphic-style-loader/lib/withStyles'
export default withStyles(s)(Login)
Login/Login.css
@import "components/App/App.css";
.root {
margin: 5px;
}
Login/route.js
import React from 'react'
import Login from './Login'
const title = `Login`
export default {
path: '/login',
action() {
return {
title,
component: <Login title={ title } />,
}
},
}
NotFound/NotFound.js
import React from 'react'
import { Button } from 'semantic-ui-react'
import { Header } from 'semantic-ui-react'
import { Segment } from 'semantic-ui-react'
import go from 'core/history/go'
import s from './NotFound.css'
const NotFound = () => (
<div className={ s.root }>
<Segment>
<Header>Page not found</Header>
</Segment>
<Button onClick={ () => go(`/`) }>Home</Button>
</div>
)
import withStyles from 'isomorphic-style-loader/lib/withStyles'
export default withStyles(s)(NotFound)
NotFound/NotFound.css
@import "components/App/App.css";
.root {
margin: 5px;
}
NotFound/route.js
import React from 'react'
import NotFound from './NotFound'
const title = `NotFound`
export default {
path: '*',
action() {
return {
title,
component: <NotFound title={ title } />,
status: 404,
}
},
}
And now we ready to create client
./src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Router from 'universal-router'
import queryString from 'query-string'
import App from 'components/App'
import history from 'core/history'
import routes from 'pages/routes'
const context = {
insertCss: (...styles) => {
const removeCss = styles.map(x => x._insertCss())
return () => { removeCss.forEach(f => f()) }
},
}
const container = document.getElementById('root')
const router = new Router(routes)
let currentLocation = history.location
async function onLocationChange(location, action) {
currentLocation = location
try {
const route = await router.resolve({
path: location.pathname,
query: queryString.parse(location.search),
})
if (currentLocation.key !== location.key) return
if (route.redirect) {
history.replace(route.redirect)
return
}
ReactDOM.render(
<App context={ context }>{ route.component }</App>,
container
)
} catch (error) {
console.error(error)
}
}
history.listen(onLocationChange)
onLocationChange(currentLocation)
Developer version is ready. U can run it by yarn start
and using routing and CSS injection without SSR.
Start to create server configuration
In ./config/paths.js
add
...
serverIndexJs: resolveApp('src/node_modules/server/server.js')
...
Copy webpack.config.prod.js
to webpack.config.server.js
and change next:
...entry: [
require.resolve('./polyfills'),
paths.serverIndexJs,
],
output: {
path: paths.appBuild,
filename: 'server.js',
libraryTarget: 'commonjs2',
},
target: 'node',
...
...
Create in ./src/node_modules/server/
server.js
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import express from 'express'
import { createServer } from 'http'
import fs from 'fs'
import Router from 'universal-router'
import App from 'components/App'
import Html from 'components/Html'
import routes from 'pages/routes'
import React from 'react'
import ReactDOM from 'react-dom/server'
const PORT = 3000
const app = express()
const server = createServer(app)
const router = new Router(routes)
const jsFiles = fs.readdirSync('./build/static/js')
app.use(express.static('./build'))
app.use(cookieParser())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.get('*', async (req, res, next) => {
try {
const css = new Set()
const context = {
insertCss: (...styles) => {
styles.forEach(style => css.add(style._getCss()))
},
}
const route = await router.resolve({
path: req.path,
query: req.query,
})
if (route.redirect) {
res.redirect(route.status || 302, route.redirect)
return
}
const data = { ...route }
data.children = ReactDOM.renderToString(<App context={ context }>{ route.component }</App>)
data.styles = [
{ id: 'css', cssText: [...css].join('') },
]
data.scripts = [
`/static/js/${ jsFiles[0] }`
]
const html = ReactDOM.renderToStaticMarkup(<Html {...data} />)
res.status(route.status || 200)
res.send(`<!doctype html>${html}`)
} catch (err) {
next(err)
}
})
app.use((err, req, res, next) => {
console.log(err)
res.status(err.status || 500)
res.send(`Internal server error`)
})
server.listen(PORT, () => {
console.log(`==> 🌎 http://0.0.0.0:${ PORT }/`)
})
Create Html component
./src/node_modules/components/Html
import PropTypes from 'prop-types'
import React from 'react'
class Html extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
styles: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
cssText: PropTypes.string.isRequired,
}).isRequired),
scripts: PropTypes.arrayOf(PropTypes.string.isRequired),
children: PropTypes.string.isRequired,
}
static defaultProps = {
styles: [],
scripts: [],
}
render() {
const { title, description, styles, scripts, children } = this.props
return (
<html className="no-js" lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="x-ua-compatible" content="ie=edge" />
<title>{title}</title>
<meta name="description" content={description} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
{styles.map(style =>
<style
key={ style.id }
id={ style.id }
dangerouslySetInnerHTML={{ __html: style.cssText }}
/>,
)}
</head>
<body>
<div
id="root"
dangerouslySetInnerHTML={{ __html: children }}
/>
{ scripts.map(script => <script key={script} src={script} />) }
</body>
</html>
)
}
}
export default Html
Add building scripts
Copy ./scripts/build.js
to ./scripts/buildServer.js
and change:
...
var config = require('../config/webpack.config.server');
...measureFileSizesBeforeBuild(paths.appBuild).then(previousFileSizes => {
// Start the webpack build
build(previousFileSizes);
});
...
Clean function build
as shown in our repository
./package.json
...
"start:ssr": "node build/server.js",
"build:server": "node scripts/buildServer.js",
...
All is done. You can run your server with SSR like this:
yarn build
yarn build:server
node start:ssr
To see:
And finally
U can see in create-react-app-ssr using of external module mind-flex
that exported with CSS through isomorphic-style-loader
It’s shared component create using previous article Best way to create npm packages with create-react-app