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`_. .. _`Webpack`: https://webpack.js.org/ For our new React project, first create a new directory ``week10`` and then inside that run .. code-block:: console $ 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): .. code-block:: json { "name": "week10", "version": "1.0.0", "description": "A basic React app", "author": "Name ", "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: .. code-block:: console $ 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: .. code-block:: jsx import React from "react"; import ReactDom from "react-dom"; function Athena() { return

Hello World!

; } ReactDom.render(, 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 ```` 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: .. code-block:: jsx (
JSX code goes here
) This in the background will be translated into .. code-block:: js 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 ``

`` 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: .. code-block:: js 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 .. code-block:: console $ 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: .. _`Babel`: https://babeljs.io/ .. code-block:: console $ 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: .. code-block:: js 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: .. code-block:: console $ 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: .. code-block:: console $ 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: .. code-block:: html <%= htmlWebpackPlugin.options.title %> <% for (var css in htmlWebpackPlugin.files.css) { %> <% } %>
<% for (var chunk in htmlWebpackPlugin.files.js) { %> <% } %> The majority of the template is a copy of the default template, the only major adition is the ``
``. 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``: .. code-block:: 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 .. code-block:: console $ 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: .. code-block:: console $ yarn add webpack-dev-server Update the ``webpack.conf.js`` to include the server: .. code-block:: 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'] }, devServer: { static: { directory: path.resolve(__dirname, "dist"), }, open: false, historyApiFallback: true, }, }; Finally, we can now run the server .. code-block:: console $ 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``: .. code-block:: 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.