Basic Component and Tooling¶
Because React does not have a prescribed structure, we are free to define our own code structure. The structure used here is driven based on what is generally considered good practice, but you are of course free to structure the code however you want. Be aware, there has been an update to Webpack that introduced several changes that are addressed in the course and implemented to the script one step at a time.
Setup¶
The lack of structure also means that React does not come with a built-in build environment (also known as “tooling”) and we will thus have to build our own. The role of the tooling is to take our clean, modern JavaScript and SASS style files and transpile them into the JavaScript and CSS that is understood by the browsers. There are a number of libraries for doing this, that all have slightly different approaches, but fundamentally are interchangeable. The one we will be using here is called Webpack.
For our new React project, first create a new directory week10
and then inside that run
$ yarn init
You can accept the defaults and it will generate a new package.json
which will hold the installation information for our tooling (and all the project dependencies). Update the package.json
so that it looks like this (obviously replace the name and e-mail with your own):
{
"name": "week10",
"version": "1.0.0",
"description": "A basic React app",
"author": "Name <Email>",
"license": "MIT",
"devDependencies": {
},
"private": true
}
You can also quick the setup up by setting the -y
flag that will create the core file with the standard settings. Note that the licencse will be set to ISC
. With that in place we can now install the basic web-pack tooling:
$ yarn add webpack webpack-cli react react-dom
First Component¶
To create our first React component, create a new directory src/js
and in that create a file athena.jsx
with the following content:
import React from "react";
import ReactDom from "react-dom";
function Athena() {
return <h1>Hello World!</h1>;
}
ReactDom.render(<Athena />, document.getElementById('app-entry-point'));
This is the bare minimum for a React application. At the top we import the React and ReactDOM libraries. The React library provides all the core functionality, while the ReactDOM library provides the functionality for loading our application into the browser. This loading into the browser is implemented below, where we use the ReactDOM.render
function to place our <Athena/>
component into the browser element with the app-entry-point
. This name is arbitrary. It is important that there is an element in the html-file that contains that id.
The main functionality is provided by the Athena
class, which is an extension of the React.Component
. Unlike Ember, where JavaScript code and templating code are kept in separate files, in React everything is contained in the same .jsx file. JSX is a JavaScript eXtension that enables the easy inclusion of templating into our JavaScript components.
The only function a React component must have is the render()
function, which must return the result of at least one React.createElement
call. Because writing templates through nested createElement
calls is hard work, React provides the JSX format, which will then in the background be converted to createElement
calls.
To specify that we are using JSX at any point in the JavaScript code we use the round brackets like this:
(
<div>JSX code goes here</div>
)
This in the background will be translated into
React.createElement('div', null, 'JSX code goes here')
Since the code in our athena.jsx
is initially just there to test our build process, we just return a <h1>
with some test text.
Basic Build¶
The next step is to build our component, transpiling it into JavaScript for the browser with Webpack. Webpack is configured through a file called webpack.conf.js
, which you should create directly in the week10
directory and into which you should place the following code:
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, 'src/js/athena.jsx'),
output: {
filename: 'athena.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: "var",
library: "Athena"
},
mode: 'development'
};
The first setting entry
defines the main class for our application (the entry point). The second output
setting defines where our component is compiled to and how it is accessible in the browser. Because browsers still do not support the various import concepts of modern JavaScript properly, we use the libraryTarget
and library
settings to configure how our component can be accessed in the browser. The libraryTarget
should always be set to var
to indicate that entry point is to be made available as a global JavaScript variable, while the library
setting defines the name of that variable. Finally the mode
setting defines how much the transpiled code is optimised and for development should always be set to development
, which does almost no optimisation.
We can now try building our new component by running
$ yarn webpack --config webpack.conf.js
Unfortunately this will not work, because by default Webpack does not know how to deal with JSX files. We thus need to add support for transpiling JSX files. In the JavaScript world, for this kind of functionality Babel has basically become the de-factor standard library. To install it run the following:
$ yarn add babel-loader @babel/core @babel/preset-env @babel/preset-react babel-plugin-transform-class-properties
Then update the webpack.conf.js
to configure the use of Babel in Webpack:
const path = require('path');
module.exports = {
entry: path.resolve(__dirname, 'src/js/athena.jsx'),
output: {
filename: 'athena.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: "var",
library: "Athena"
},
mode: 'development',
module: {
rules: [
{
test: /\.m?jsx$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['babel-plugin-transform-class-properties']
}
}
}
]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
};
In Webpack additional transpilation modules are defined in the module
section via rules that specify when to apply a certain transpilation. Here we use the test
property to define a regular expression that matches all jsx files. We also set the exclude
to exclude the node_modules
directory, as we do not need to re-transpile existing libraries, which would slow things down significantly. Finally the config specifies that for all JSX files we find, we use the Babel loader with a certain set of presets, which in Babel terminology is the name for sets of rules that define how the source is transpiled into the output.
You should now be able to run Webpack to transpile our component:
$ yarn webpack --config webpack.conf.js
You will see that in the dist
directory a bundle.js
file has appeard, which contains the transpiled source. If you want, you can have a look at that file to see that it contains both the React library code and our own code (which will be at the end of the file).
Viewing the Component¶
To actually view the component in action we require a HTML file that loads it and luckily there is a Webpack plugin that helps with that:
$ yarn add html-webpack-plugin
While the plugin will generate a HTML file that loads our code, because we need to install the component in the page, we need to add some specific code to the HTML file and that is achieved through a template. Create a new file src/index.html
and add the following code:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8"/>
<title><%= htmlWebpackPlugin.options.title %></title>
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/2.8.94/css/materialdesignicons.min.css"/>
<% for (var css in htmlWebpackPlugin.files.css) { %>
<link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet"/>
<% } %>
</head>
<body>
<div id="app-entry-point"></div>
<% for (var chunk in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[chunk] %>"></script>
<% } %>
</body>
</html>
The majority of the template is a copy of the default template, the only major adition is the <div id="app-entry-point"></div>
. This specifies where in the page our component will be installed.
Next, we need to tell Webpack to use this template by updating the webpack.conf.js
:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: path.resolve(__dirname, 'src/js/athena.jsx'),
output: {
filename: 'athena.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: "var",
library: "Athena"
},
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
title: 'Athena',
template: path.resolve(__dirname, 'src/index.html'),
inject: false,
xhtml: true
})
],
module: {
rules: [
{
test: /\.m?jsx$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['babel-plugin-transform-class-properties']
}
}
}
]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
};
If you now run
$ yarn webpack --config webpack.conf.js
you will see that the dist
directory now also contains a index.html
file. If you open this in the browser, you will see the component’s output displayed. This works, but the browser places certain limitations on pages displayed directly from file, thus we want to include a build server, similar to what we had with Ember:
$ yarn add webpack-dev-server
Update the webpack.conf.js
to include the server:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: path.resolve(__dirname, 'src/js/athena.jsx'),
output: {
filename: 'athena.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: "var",
library: "Athena"
},
mode: 'development',
plugins: [
new HtmlWebpackPlugin({
title: 'Athena',
template: path.resolve(__dirname, 'src/index.html'),
inject: false,
xhtml: true
})
],
module: {
rules: [
{
test: /\.m?jsx$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['babel-plugin-transform-class-properties']
}
}
}
]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
devServer: {
static: {
directory: path.resolve(__dirname, "dist"),
},
open: false,
historyApiFallback: true,
},
};
Finally, we can now run the server
$ yarn webpack-dev-server --config webpack.conf.js
and the our component will be available from http://localhost:8080
Code splitting¶
If you look at the output from running webpack-dev-server
(or webpack
), you will see that it always outputs which assets were generated and how big they are. If you look at the file-size, you will see that our “bundle.js”, which contains everything, is relatively large. We can reduce this by switching to production mode, which for the final deployment, we would do, but we can also consider which pieces of code are updated how frequently.
The libraries that we use (React, ReactDOM, and the things we will add in the next tutorial) are updated regularly, but probably not as frequently as we will deploy changes to our own application. We can thus split the library code from our own code into two separate JavaScript files and then tell the browser to cache the library code, which will significantly speed up the loading process for our application. To do this, we use Webpacks optimization
feature, configured in the webpack.conf.js
:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: path.resolve(__dirname, "src/js/athena.jsx"),
output: {
filename: "[name]-bundle.js",
path: path.resolve(__dirname, "dist"),
libraryTarget: "var",
library: "Athena",
},
mode: "development",
plugins: [
new HtmlWebpackPlugin({
title: "Athena",
template: path.resolve(__dirname, "src/index.html"),
inject: false,
xhtml: true,
}),
],
module: {
rules: [
{
test: /\.m?jsx?$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
plugins: ["babel-plugin-transform-class-properties"],
},
},
},
],
},
resolve: {
extensions: ["*", ".js", ".jsx"],
},
devServer: {
static: {
directory: path.resolve(__dirname, "dist"),
},
open: false,
historyApiFallback: true,
},
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
commons: {
test: /node_modules/,
name: "athena-vendor",
chunks: "initial",
minSize: 1,
},
},
},
},
};
If you now re-start the build server, you will see that two JavaScript assets are output. One the large athena-vendor-bundle.js
which contains the library code and the other main-bundle.js
with our own application code.