React app from scratch

Evheniy Bystrov
Dec 31, 2017 · 17 min read

It’s a first part of tutorial where I’m going to show how to create react app from scratch.

React is not a framework. It’s a JavaScript library for building user interfaces. If you want to create react app you probably should create own framework based on react — you need to manage state, work with async actions, create own component structure…


Codesandbox.io

If you want to create your first component or just see what is react — you can use service https://codesandbox.io/.

Next I’ll show you how to work with react on your PC.


You need to install node.js. I use nvm and docker. About docker you can read my article:

Create-react-app

If you just want to see what is react and create small app you can use create-react-app. It’s not perfect choice for production because on real app you need to manage state, work with async actions (ajax requests)… In this case you mostly need webpack or other bundler. But for running your first hello world app on PC create-react-app is a good choise.

After installing node.js, npm helps you to install a new packages. To work with create-react-app you need to install it globally using next command:

npm i -g create-react-app

Create a new project:

create-react-app my-app

And start it:

cd my-app/
npm start

Your project structure:

You can play with it but for real production you need to use other way.

Next I’ll show you react ecosystem: webpack, babel, npm scripts, how to use hot module replacement (HMR) with dev server and service workers with workbox.

Let’s start…


Create a new directory for our app:

mkdir app
cd app

Init project

Any node.js project should start from npm init command:

npm init -f

EditorConfig

EditorConfig is a file format and collection of text editor plugins for maintaining consistent coding styles between different editors and IDEs.

EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs. The EditorConfig project consists of a file format for defining coding styles and a collection of text editor plugins that enable editors to read the file format and adhere to defined styles. EditorConfig files are easily readable and they work nicely with version control systems.

To use it just create .editorconfig file and be sure that your IDE works with it:

# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Set default charse
charset = utf-8

# 2 space indentation
indent_style = space
indent_size = 2

Eslint

Next we need to configure eslint.

ESLint is an open source project originally created by Nicholas C. Zakas in June 2013. Its goal is to provide a pluggable linting utility for JavaScript.

We will use Airbnb eslint react config. To install it just run:

npm i -D eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react

We use flag -D to install it as dev dependency.

After we need to create .eslintrc file with settings:

{
"extends": "airbnb",
"env": {
"browser": true,
"node": true,
"mocha": true
}
}

And add lint command to package.json scripts section:

"scripts": {
"test": "eslint ."
},

To run it use npm run command:

npm test

You can configure your IDE for using eslint and auto checking.

Npm Task List

Each command from scripts section you can run using npm run command. For example npm run test, npm run start. Some useful commands like test and start you can run using short syntax: npm test (or even npm t) and npm start.

But if you work with a new project and you don’t want to open package.json file you can use npm task list (ntl). Just install it globally:

npm i -g ntl

After that run ntl command in app directory and you can click up/down arrow buttons to select command:

npm-check-updates

Other useful command if you need to check and update dependency.

npm-check-updates is a command-line tool that allows you to upgrade your package.json dependencies to the latest versions, regardless of existing version constraints.

To install it globally run

npm i -g npm-check-updates

After that run ncu command.

We can downgrade eslint command to see how it works:

Run ncu -u, it will upgrade package.json

And run npm i to install new dependency

Browserlist

Browserlist helps to share target browsers between different front-end tools, like Autoprefixer, Stylelint and babel-preset-env.

We will use it for postcss and babel-preset-env.

Just create .browserslistrc file and put configs there:

# Browsers that we support

> 1%
Last 2 versions
IE 10 # sorry

You can use online demo for testing Browserslist queries or use babel repl to test env preset:

You can see targets and plugins added to app for workings with browser targets.

Babel

Babel is compiler for writing next generation JavaScript. It helps to transform react jsx and es6 code to old es5 code. It helps to use a new JS standards for old browsers.

To use it on node.js side we need to install babel-core. As we discussed before, we need to install babel-preset-env with babel-polyfill. And for react we need babel-preset-react.

And one more. I’m going to use a new features like object spread/rest operator and static object properties. For this I need to install babel-plugin-transform-object-rest-spread and babel-plugin-transform-class-properties.

And we can use babel-plugin-lodash — a simple transform to cherry-pick Lodash modules.

We need it only for development:

npm i -D babel-core babel-preset-env babel-preset-react babel-plugin-transform-class-properties babel-plugin-transform-object-rest-spread babel-plugin-lodash

But babel-polyfill with react-hot-loader we will use in a code, so it’s not dev dependency. We should use flag -S:

npm i -S babel-polyfill react-hot-loader

Next we need to install configuration file: .babelrc

{
"presets": ["env", "react"],
"plugins": [
"transform-class-properties",
"transform-object-rest-spread",
"react-hot-loader/babel",
"lodash"
]
}

As you can see I added react-hot-loader/babel as plugin. We need it to work with hot module replacement.

PostCSS

PostCSS is a tool for transforming styles with JS plugins. These plugins can lint your CSS, support variables and mixins, transpile future CSS syntax, inline images, and more.

PostCSS is used by industry leaders including Wikipedia, Twitter, Alibaba, and JetBrains. The Autoprefixer PostCSS plugin is one of the most popular CSS processors.

We will use postcss-cssnext and cssnano.

PostCSS-cssnext is a PostCSS plugin that helps you to use the latest CSS syntax today. It transforms CSS specs into more compatible CSS so you don’t need to wait for browser support.

Cssnano takes your nicely formatted CSS and runs it through many focused optimisations, to ensure that the final result is as small as possible for a production environment.

To install it run:

npm i -D postcss postcss-cssnext cssnano

Postcss-cssnext has an interesting tool — autoprefixer. It’s postcss plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use. It is recommended by Google and used in Twitter and Taobao.

And it uses Browserlist rules.

And config file: postcss.config.js

module.exports = {
plugins: {
'postcss-cssnext': {
warnForDuplicates: false,
},
cssnano: {},
},
};

Webpack

Webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.

What we will use with webpack:

To install it run:

npm i -D webpack webpack-dev-server copy-webpack-plugin babel-loader postcss-loader style-loader css-loader sass-loader node-sass file-loader extract-text-webpack-plugin html-webpack-plugin clean-webpack-plugin webpack-bundle-analyzer lodash-webpack-plugin workbox-webpack-plugin

Next I’m going to split webpack config into small single responsibility files. Let’s create webpack directory:

mkdir webpack
cd webpack

webpack/index.js

const { resolve } = require('path');const vendor = require('./vendor');
const rules = require('./rules');
const plugins = require('./plugins');
const devServer = require('./dev_server');
const devtool = require('./devtool');
const settings = {
resolve: {
extensions: ['*', '.js', '.jsx', '.css', '.scss'],
},
context: resolve(__dirname, '..'),
entry: {
app: [
'react-hot-loader/patch',
'babel-polyfill',
'./src/index'
],
vendor,
},
output: {
filename: '[name].[hash].js',
path: resolve(__dirname, '..', 'dist'),
},
module: {
rules,
},
plugins,
devServer,
devtool,
};
module.exports = settings;

webpack/vendor.js

const vendor = [
'babel-polyfill',
'react',
'react-dom',
'react-redux',
'react-router',
'react-router-redux',
'react-virtualized',
'redux',
'redux-observable',
'react-toolbox',
'react-hot-loader',
'rxjs',
'lodash',
'moment',
'localforage',
'react-loadable',
];
module.exports = vendor;

Here we can put libraries which we don’t want to put in the main code. It helps to split one big app file into smaller files for entry points and vendors. Vendors can be cached between releases if we don’t upgrade them.

All packages we will install later.

webpack/rules.js

const { join } = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const rules = [{
test: /.jsx?$/,
loader: 'babel-loader',
exclude: /node_modules/,
}, {
test: /\.scss$/,
exclude: /node_modules/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 2,
modules: true,
localIdentName: '[name]__[local]___[hash:base64:5]'
},
}, {
loader: 'postcss-loader',
}, {
loader: 'sass-loader',
options: {
sourceMap: true,
},
}],
}),
}, {
test: /\.css$/,
use: [{
loader: 'style-loader',
}, {
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 2,
modules: true,
localIdentName: '[name]__[local]___[hash:base64:5]'
},
}, {
loader: 'postcss-loader',
}],
}, {
test: /\.(woff2|woff|ttf|eot|svg)(\?.*$|$)/,
loader: 'file-loader?name=fonts/[name].[ext]',
include: [
join(__dirname, 'src'),
join(__dirname, 'node_modules'),
],
}, {
test: /\.(jpg|jpeg|gif|png|ico)(\?.*$|$)$/,
loader: 'file-loader?name=img/[name].[ext]',
include: [
join(__dirname, 'src'),
join(__dirname, 'node_modules'),
],
}];
module.exports = rules;

Here we described rules for loading different kind of files like js/jsx, scss, css, fonts, images.

webpack/plugins.js

const { resolve, join } = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const isProduction = process.env.NODE_ENV === 'production';const dist = 'dist';// the path(s) that should be cleaned
const pathsToClean = [
`${dist}/*.*`,
];
// the clean options to use
const cleanOptions = {
root: resolve(__dirname, '..'),
exclude: [`${dist}/.gitignore`],
verbose: true,
dry: false,
};
const plugins = [
new webpack.EnvironmentPlugin({ NODE_ENV: 'development' }),
new CleanWebpackPlugin(pathsToClean, cleanOptions),
new LodashModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: join('src', 'index.html'),
}),
new ExtractTextPlugin(join(dist, 'bundle.css'), {
allChunks: true,
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: Infinity,
}),
new webpack.NamedModulesPlugin(),
];
if (isProduction) {
plugins.push(
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false,
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: true,
compress: {
warnings: false,
screw_ie8: true,
conditionals: true,
unused: true,
comparisons: true,
sequences: true,
dead_code: true,
evaluate: true,
if_return: true,
join_vars: true,
},
output: {
comments: false,
},
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new CopyWebpackPlugin([{
from: require.resolve('workbox-sw'),
to: 'workbox-sw.prod.js',
}]),
new WorkboxPlugin({
globDirectory: dist,
globPatterns: ['**/*.{html,js,css}'],
swSrc: join('src', 'service-worker.js'),
swDest: join(dist, 'service-worker.js'),
clientsClaim: true,
skipWaiting: true,
navigateFallback: '/index.html',
})
);
} else {
plugins.push(
new webpack.LoaderOptionsPlugin({
debug: true,
}),
new webpack.HotModuleReplacementPlugin(),
new BundleAnalyzerPlugin()
);
}
module.exports = plugins;

In this file we configure webpack plugins and split it by environment — production or development. For development we need hot module replacement and analyzer, for production we need workbox plugin and uglifyjs.

webpack/devtool.js

const isProduction = process.env.NODE_ENV === 'production';const devtool = isProduction ? 'source-map' : 'inline-cheap-module-source-map';module.exports = devtool;

Here we just check environment and choose what kind of source map we should use.

webpack/dev_server.js

const { join } = require('path');const devServer = {
quiet: false,
port: 3000,
contentBase: join(__dirname, '..', 'dist'),
hot: true,
historyApiFallback: true,
inline: true,
noInfo: false,
headers: { 'Access-Control-Allow-Origin': '*' },
stats: {
assets: false,
colors: true,
version: false,
hash: false,
timings: false,
chunks: false,
chunkModules: false,
},
};
module.exports = devServer;

Here we set config for webpack dev server.

webpack.config.js

require('webpack');const settings = require('./webpack');module.exports = settings;

And last thing — adding scripts to package.json:

"scripts": {
"test": "eslint .",
"start": "webpack-dev-server --inline",
"build": "NODE_ENV=production webpack"
},

Small update — we don’t need to check webpack files by eslint, so we need to create .eslintignore file and put next lines (about dist and service-worker later):

webpack/*.js
dist/*.js
src/service-worker.js

Now run npm test command or use ntl to lint project files:

React

Now we can install all our dependency.

  • ReactI hope now you know what is it ;)
  • React-DOM — This package serves as the entry point of the DOM-related rendering paths.
  • Redux — a predictable state container for JavaScript apps.
  • react-redux — official React bindings for Redux.
  • RxJS — a library for reactive programming using Observables, to make it easier to compose asynchronous or callback-based code.
  • redux-observable — RxJS middleware for action side effects in Redux using “Epics”.
  • react-router — declarative routing for react.
  • react-router-redux — ruthlessly simple bindings to keep react-router and redux in sync.
  • react-virtualized — React components for efficiently rendering large lists and tabular data.
  • react-toolbox — a set of React components implementing Google’s Material Design specification with the power of CSS Modules http://www.react-toolbox.io.
  • Lodash — a JavaScript utility library delivering consistency, modularity, performance, & extras.
  • moment — parse, validate, manipulate, and display dates and times in JavaScript.
  • localForage — offline storage, improved. Wraps IndexedDB, WebSQL, or localStorage using a simple but powerful API.
  • react-loadable — a higher order component for loading components with promises.

All those packages we will use in next part.

To install it use next command:

npm i -S react react-dom redux react-redux rxjs redux-observable react-router react-router-redux react-virtualized react-toolbox lodash moment localforage react-loadable

Our code we will store in src directory. Let’s create it:

mkdir src
cd src

As we use html-webpack-plugin we need to create index.html:

src/index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Reports</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div id="root"></div>
</body>
</html>

Here I added link to Google fonts because react-toolbox doesn’t include fonts into package. And <div id=”root”></div> is our app container. Html-webpack-plugin will add script tag after container.

And our entry point:

src/index.jsx

import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import App from './components/App';
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById('root'),
);
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('service-worker.js').catch(() => {});
});
}
};
render(App);if (module.hot) {
module.hot.accept();
}

Here I include react with react-hot-loader and link to our react component.

After I created function for rendering our component using hot module replacement. Next I added code to work with service workers, but I’ll describe it later.

I run our function render and after I check hot module replacement for changing code.

And create our first react component.

mkdir components
cd components

src/components/App.jsx

import React, { Component } from 'react';
import { AppBar, Checkbox, IconButton } from 'react-toolbox';
import { Layout, NavDrawer, Panel, Sidebar } from 'react-toolbox';
class App extends Component {
state = {
drawerActive: false,
drawerPinned: false,
sidebarPinned: false
};
toggleDrawerActive = () => {
this.setState({ drawerActive: !this.state.drawerActive });
};
toggleDrawerPinned = () => {
this.setState({ drawerPinned: !this.state.drawerPinned });
};
toggleSidebar = () => {
this.setState({ sidebarPinned: !this.state.sidebarPinned });
};
render() {
return (
<Layout>
<NavDrawer active={this.state.drawerActive}
pinned={this.state.drawerPinned} permanentAt='xxxl'
onOverlayClick={ this.toggleDrawerActive }>
<p>
Navigation, account switcher, etc. go here.
</p>
</NavDrawer>
<Panel>
<AppBar title="React Toolbox" leftIcon="menu" onLeftIconClick={ this.toggleDrawerActive } />
<div style={{ flex: 1, overflowY: 'auto', padding: '1.8rem' }}>
<h1>Main Content</h1>
<p>Main content goes here.</p>
<Checkbox label='Pin drawer' checked={this.state.drawerPinned} onChange={this.toggleDrawerPinned} />
<Checkbox label='Show sidebar' checked={this.state.sidebarPinned} onChange={this.toggleSidebar} />
</div>
</Panel>
<Sidebar pinned={ this.state.sidebarPinned } width={ 5 }>
<div><IconButton icon='close' onClick={ this.toggleSidebar }/></div>
<div style={{ flex: 1 }}>
<p>Supplemental content goes here.</p>
</div>
</Sidebar>
</Layout>
);
}
}
module.exports = App;

It’s just an example. I took it from react toolbox documentation.

Workbox

Workbox is a collection of libraries and build tools that make it easy to store your website’s files locally, on your users’ devices. Consider Workbox if you want to:

  • Make your site work offline.
  • Improve load performance on repeat-visits. Even if you don’t want to go fully-offline, you can use Workbox to store and serve common files locally, rather than from the network.

We use workbox webpack plugin (we already installed it and added it to webpack/plugins.js). Now we need to install workbox-sw — a service worker library to make managing fetch requests and caching as easy as possible. It provides a high-level wrapper on top of a number of individual modules, giving you a consistent, powerful interface.

npm i -D workbox-sw

To work with workbox-sw we already added another plugin — copy-webpack-plugin. It should copy workbox-sw module to dist/workbox-sw.prod.js where we have our files after building.

Last thing — creating service-worker file where we can set rules for caching:

src/service-worker.js

importScripts('workbox-sw.prod.js');const workboxSW = new self.WorkboxSW({
"skipWaiting": true,
"clientsClaim": true,
});
workboxSW.precache([]);workboxSW.router.registerRoute('https://fonts.googleapis.com/(.*)',
workboxSW.strategies.cacheFirst({
cacheName: 'googleapis',
cacheExpiration: {
maxEntries: 20,
},
cacheableResponse: { statuses: [0, 200] },
})
);
// We want no more than 50 images in the cache. We check using a cache first strategy
workboxSW.router.registerRoute(/\.(?:png|gif|jpg)$/,
workboxSW.strategies.cacheFirst({
cacheName: 'images-cache',
cacheExpiration: {
maxEntries: 50,
},
})
);
workboxSW.router.registerRoute(/index.html/, workboxSW.strategies.staleWhileRevalidate());

Here I cache Google fonts, app images and index.html — main app page.

And structure of our project:

How to use

First we need to create dist directory and add .gitkeep file (we will use it later):

mkdir dist
touch dist/.gitkeep

Testing

As we added dist directory to .eslintignore we should not test our built files. The same with src/service-worker.js.

So we can run npm test or use ntl to check that we don’t have any problems:

Development

Command npm start will run webpack dev server with configs from webpack/dev_server.js.

It helps us with 2 main problems — building our files with caching them into memory and hot module replacement (HMR). If you make changes in any file (js, css, scss…) you can see it almost at the same time in browser without refreshing page.

After running npm start or using ntl you can see opened page with analyzer:

Here you can see bundle content as convenient interactive zoomable treemap.

This module will help you:

  1. Realize what’s really inside your bundle
  2. Find out what modules make up the most of it’s size
  3. Find modules that got there by mistake
  4. Optimize it!

And the best thing is it supports minified bundles! It parses them to get real size of bundled modules. And it also shows their gzipped sizes!

It’s a good start to make your app less and faster.

But it’s not our goal now. Let’s open http://localhost:3000/ and check our app:

If we open code and make any change:

We can see it at the same time on page:

It really helps in development.

Build production ready bundles

If we need to release our app we need to generate (build) files before put it on server or CDN.

Just run npm run build or use ntl:

We can see a new files:

We can run index.html file in any web server for example http-server.

Install it:

npm i -g http-server

And run it from dist directory:

cd dist
http-server

And open http://localhost:8080/

We see the same content. But if we open development tools and open Application tab with Service Workers menu we can se installed service worker:

We can test it — set offline checkbox and refresh page:

It works the same. If we open Network tab and again refresh page:

We can see that all our files we get from service worker. It’s our goal.

Github

I created github repository so you can clone code on your PC (don’t forget to install node.js and install all dependency) and run it.

In next part I’ll show you how to work with redux, redux-observable, react-loadable… And how to create tests to make your app 100% production ready.

My other posts

References

HackerNoon.com

how hackers start their afternoons.

Evheniy Bystrov

Written by

Technical Lead

HackerNoon.com

how hackers start their afternoons.