The other day I was wondering about how many steps it takes to configure a React/Redux boilerplate?, So I decided to initiate the creation of this step by step guide. This is still a work in progress...
This guide supposes the user has some experience with the Command Line Interface for npm and git.
- Initial Configuration
- Configuring React
- Configuring the Server
- Hot Module Reloading (HMR)
- Development configurations
- Unit Testing
- Redux
- Server Side Rendering
- nvm https://github.com/creationix/nvm#installation , use the CURL option
- avn https://github.com/wbyoung/avn#install
- oh-my-zsh https://github.com/robbyrussell/oh-my-zsh
In your project root directory create the file .gitignore with the content:
.DS_STORE
/node_modules
/dist
package-lock.json
This will specify intentionally untracked files to ignore.
npm init --yes
And add the node semver number (9.8.0 at the time of writing this Readme)
9.8.0
Make Atom to ignore node_modules. Since we have not configured any GIT repository yet, Atom will read the entire node_modules directory. Which will cause some plugins/packages to fail. So, go ahead to the Preferences and add node_modules in the Ignored Names list section.
npm i eslint --save-dev
eslint --init
And use this configuration
How would you like to configure ESLint? Use a popular style guide
Which style guide do you want to follow? AirBnB
What format do you want your config file to be in? JavaScript
- eslint-plugin-react
npm i eslint-plugin-react@latest --save-dev
- eslint-plugin-jsx-a11y
npm i eslint-plugin-jsx-a11y@latest --save-dev
- eslint-plugin-import
npm i eslint-plugin-import@latest --save-dev
npm i webpack webpack-cli --save-dev
const path = require('path');
module.exports = {
entry: './src/client/App.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.bundle.js',
},
resolve: {
extensions: ['.js', '.jsx'],
},
};
Up to this point, we have Webpack 4 ready to bundle JavaScript code! Create the src/client/App.jsx file and put some Vanilla Javascript code, let's say:
console.log(1234567890);
Then run:
webpack --mode development
This will create the bundle under the ./dist directory.
npm i react react-dom --save
npm install --save-dev babel-loader babel-core
We will now include the babel loader as follow:
module: {
rules: [
{
test: /\.jsx$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
}
Notice the exclude property, this will prevent Webpack to apply the loader on the node_modules directory
Which enables transforms for ES2015+
npm install babel-preset-env --save-dev
{
"presets": ["env"]
}
npm install --save-dev babel-cli babel-preset-react
{
"presets": ["env", "react"]
}
Up to this point, we already have our initial Webpack 4 configuration with support for:
- ES15+
- React
- Eslint
npm i express --save
npm i webpack-dev-middleware --save-dev
The server script should be placed at this path: /src/server/index.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('../../webpack.config.js');
const compiler = webpack(config);
const app = express();
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath,
}));
app.listen(3000, () => {
console.log('Server running at port 3000');
});
We now have a Web server running on the port 3000. And any file change will trigger the build process, and the Server will be updated automatically. To see this in action do this:
- Run the Server: node ./src/server
- Visit http://localhost:3000/app.bundle.js. You should see the bundle created by Webpack
- Look for: console.log(1234567890)
- Update the file ./src/client/App.jsx to console.log(12345678900)
- Refresh http://localhost:3000/app.bundle.js
- Look for: console.log(12345678900)
Install this new dev package
npm i webpack-hot-middleware --save-dev
Add it to the server script:
const webpackHotMiddleware = require('webpack-hot-middleware');
app.use(webpackHotMiddleware(compiler));
Add the following plugin in the webpack.config.js file
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
Add a new entry in the webpack.config.js file
entry: [
'./src/client/App.jsx',
'webpack-hot-middleware/client',
]
Add this inside the ./src/client/App.jsx file
if (module.hot) {
module.hot.accept();
}
Up to this point any change we do in ./src/client/App.jsx will be automatically reflected in the browser, with no page refresh needed.
Add this line in the webpack.config.js file
mode: 'development',
Add this line in the webpack.config.js file
devtool: 'cheap-module-eval-source-map'
Make sure the Enable JS source maps option is enabled as per Chrome documentation: enable_source_maps_in_settings
Restart the server, you should then be able to see the project original files:
Install Source Map Explorer npm package
npm i source-map-explorer --save-dev
Add:
mode: env.NODE_ENV === 'development' ? 'development' : 'production',
devtool: env.NODE_ENV === 'development' ? 'cheap-module-eval-source-map' : 'source-map',
Convert module.exports to a function:
module.exports = env => (
{
mode: env.NODE_ENV === 'development' ? 'development' : 'production',
devtool: env.NODE_ENV === 'development' ? 'cheap-module-eval-source-map' : 'source-map',
/* The rest of the configuration goes here.
.
.
.
*/
}
);
"source-map-explorer-production": "webpack --env.NODE_ENV=production && ./node_modules/.bin/source-map-explorer ./dist/app.bundle.js"
Now you can analyze the bundle of the App running:
npm source-map-explorer-production
The source map explorer determines which file each byte in your minified code came from. It shows you a treemap visualization to help you debug where all the code is coming from.
By default we have set the production mode to work with the source-map-explorer package. Try to change the webpack.config.js to development, to see the difference in size of both configurations.
mode: 'development'
Production: ~162Kb
Development: ~717Kb
Install attrs.argv
npm install attrs.argv
Update the Script server (/src/server/index.js):
var argv = require('attrs.argv');
// CLI Arguments
const { NODE_ENV } = argv;
// Webpack Configuration Object
const webpackConfig = config({
env: {
NODE_ENV,
},
});
const compiler = webpack(webpackConfig);
Also update the publicPath
publicPath: webpackConfig.output.publicPath,
"server-dev": "node ./src/server NODE_ENV=development"
You should be able to run the development server
npm run server-dev
npm i jest --save-dev
Place your tests in a tests folder, or name your test files with a .spec.js or .test.js extension. Whatever you prefer, Jest will find and run your tests.
Following Jest documentation we will create a new directory named utils with the following structure (/src/client/utils):
|-- src
|-- client
| |-- utils
| |-- arrays.js
| |-- __tests__
| |-- arrays.test.js
const concat = (arrayOne, arrayTwo) => (
[...arrayOne, ...arrayTwo]
);
const lastElement = array => (
array[array.length - 1]
);
export {
concat,
lastElement,
};
import * as utils from '../arrays';
describe('Arrays', () => {
test('concat', () => {
const arrayOne = [1, 2, 3];
const arrayTwo = [4, 5, 6];
expect(utils.concat(arrayOne, arrayTwo)).toEqual([1, 2, 3, 4, 5, 6]);
});
test('Last item', () => {
const receivedArray = ['one', 'two', 'three'];
expect(utils.lastElement(receivedArray)).toBe('three');
});
});
"test": "./node_modules/.bin/jest --watch",
Execute the tests:
npm un test
npm i --save-dev enzyme enzyme-adapter-react-16
Create the following structure and files (/src/client/components/header/):
|-- src
|-- client
| |-- components
| | |-- header
| | |-- Header.jsx
| | |-- index.js
| | |-- __tests__
| | |-- Header.test.js
import React from 'react';
const Header = () => (
<h1>Header!</h1>
);
export default Header;
import Header from './Header.jsx';
export default Header;
import React from 'react';
import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import Header from '../Header';
Enzyme.configure({ adapter: new Adapter() });
describe('Header (component)', () => {
it('has at least one <h1> tag', () => {
const wrapper = shallow(<Header />);
expect(wrapper.contains(<h1>Header!</h1>)).toBe(true);
})
});
Execute the test script and Enzyme will run over Jest:
npm run tests
npm i redux --save
npm i react-redux --save
./src/client/index.jsx
import React from 'react';
import reactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './App';
import reducers from './reducers';
if (module.hot) {
module.hot.accept();
}
const store = createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
);
const AppConfiguration = () => (
<Provider store={store}>
<App />
</Provider>
);
reactDOM.render(<AppConfiguration />, document.getElementById('root'));
Create the following file structure for the reducers configuration:
|-- src
|-- reducers
| |-- index.js
| |-- inventory.js
./src/client/reducers/index.js
import { combineReducers } from 'redux';
import inventory from './inventory';
export { inventory };
export default combineReducers({
inventory,
});
./src/client/reducers/inventory.js
const inventory = (state = {}) => state;
export default inventory;
./src/client/App.jsx
import React from 'react';
import Header from './components/header';
const App = () => (
<div>
<Header />
</div>
);
export default App;
Install the Chrome Addon Redux DevTools, so you can see the Store state structure. By now we only have the inventory property with an empty object:
We will organize a little bit our scripts and configurations, such way we have the Development and Configuration settings separated. Basically the Server Side Rendering magic happens in the Production Server file.
src/server/index.development.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const webpackConfig = require('../../webpack.server.development.config.js');
const compiler = webpack(webpackConfig);
const app = express();
app.use(webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath,
hot: true,
stats: {
colors: true,
},
}));
app.use(webpackHotMiddleware(compiler));
app.get('/', (req, res) => {
const html = `
<html>
<head>
</head>
<body>
<div id="root"></div>
<script src="http://localhost:3000/app.client.bundle.js"></script>
</body>
</html>
`;
res.send(html);
});
app.listen(3000, () => {
console.log('Server running at port 3000');
});
src/server/index.production.js
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducers from '../client/reducers';
import App from '../client/App';
const express = require('express');
const app = express();
app.use(express.static('./dist/public'));
function renderFullPage(html, preloadedState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="root">${html}</div>
<script>
// WARNING: See the following for security issues around embedding JSON in HTML:
// http://redux.js.org/recipes/ServerRendering.html#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
</script>
<script src="./js/app.client.bundle.js"></script>
</body>
</html>
`;
}
function handleRequest(req, res) {
const store = createStore(reducers);
const html = ReactDOMServer.renderToString(<Provider store={store}><App /></Provider>);
const preloadedState = store.getState();
res.send(renderFullPage(html, preloadedState));
}
app.use(handleRequest);
// Listen for connections (Start the server)
app.listen(3000, () => {
console.log('Server running at port 3000');
});
const path = require('path');
module.exports = {
mode: 'production',
target: 'web',
entry: path.resolve(__dirname, './src/client/index.jsx'),
output: {
path: path.resolve(__dirname, './dist/public/js'),
filename: 'app.client.bundle.js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
stats: 'none',
resolve: {
extensions: ['.js', '.jsx'],
},
};
const path = require('path');
const webpack = require('webpack');
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
entry: [path.resolve(__dirname, './src/client/index.jsx'), 'webpack-hot-middleware/client'],
output: {
path: path.resolve(__dirname, './src/server/static'),
filename: 'app.client.bundle.js',
publicPath: '/',
},
module: {
rules: [
{
test: /\.jsx$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
plugins: [new webpack.HotModuleReplacementPlugin()],
stats: 'none',
resolve: {
extensions: ['.js', '.jsx'],
},
};
const path = require('path');
module.exports = {
target: 'node',
mode: 'production',
entry: path.resolve(__dirname, './src/server/index.production.js'),
output: {
path: path.resolve(__dirname, './dist'),
filename: 'app.server.bundle.js',
publicPath: '/public/',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
stats: 'none',
resolve: {
extensions: ['.js', '.jsx'],
},
};
Let's add the following scripts in the package.json file:
Will run the application using the configuration done in the section Development configurations
"server-development": "node ./src/server/index.development.js NODE_ENV=development",
Will transpile both the Client and Server scripts, and then run the Express server.
"server-production": "npm run client-production && webpack --config webpack.server.production.config.js && node ./dist/app.server.bundle.js",
Will transpile only the Client code.
"client-production": "webpack --config webpack.client.production.config.js"
Summarizing: