Setting up React
The development environment for our project is ready. In this section, we are going to install and configure React, which is the primary aspect of this chapter. Let's start by creating a new directory for our project:
mkdir ~/graphbook cd ~/graphbook
Our project will use Node.js and many npm packages. We will create a package.json
file to install and manage all the dependencies for our project. It stores information about the project, such as the version number, name, dependencies, and much more.
Just run npm init
to create an empty package.json
file:
npm init
npm will ask some questions, such as asking for the package name, which is, in fact, the project name. Enter Graphbook
to insert the name of your application in the generated package.json
file.
I prefer to start with version number 0.0.1 since the default version number that npm offers with 1.0.0 represents the first stable release for me. However, it is your choice regarding which version you use here.
You can skip all the other questions by pressing the Enter key to save the default values of npm. Most of them are not relevant because they just provide information such as a description or the link to the repository. We are going to fill in the other fields, such as the scripts, while working through this book. You can see an example of the command line in the following screenshot:
The first and most crucial dependency for this book is React. Use npm to add React to our project:
npm install --save react react-dom
This command installs two npm packages from https://npmjs.com into our project folder under node_modules
.
npm automatically edited our package.json
file since we provided the --save
option and added those packages with the latest available version numbers.
You might be wondering why we installed two packages, even though we only needed React. The react
package only provides React-specific methods. All React Hooks, such as componentDidMount
, useState
, and even React's component class, come from this package. You need this package to write any React application.
In most cases, you won't even notice that you have used react-dom
. This package offers all the functions to connect the actual DOM of the browser to your React application. Usually, you use ReactDOM.render
to render your application at a specific point in your HTML and only once in your code. We will cover how to render React later in this book.
There is also a function called ReactDOM.findDOMNode
, which gives you direct access to a DOMNode, but I strongly discourage using this since any changes on DOMNodes are not available in React itself. I have never needed to use this function, so try to avoid it if possible. Now, that our npm project has been set up and the two main dependencies have been installed, we need to prepare an environment that bundles all the JavaScript files we are going to write. We will focus on this in the next section.
Preparing and configuring webpack
Our browser requests an index.html
file when accessing our application. It specifies all the files that are required to run our application. We need to create this index.html
file, which we serve as the entry point of our application:
- Create a separate directory for our
index.html
file:mkdir public cd public touch index.html
- Then, save this inside
index.html
:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Graphbook</title> </head> <body> <div id="root"></div> </body> </html>
As you can see, no JavaScript is loaded here. There is only one div
tag with the root
ID. This div
tag is the DOMNode that our application will be rendered in by ReactDOM
.
So, how do we get React up and running with this index.html
file?
To accomplish this, we need to use a web application bundler, which will prepare and bundle all our application assets. All the required JavaScript files and node_modules
are bundled and minified; SASS and SCSS preprocessors are transpiled to CSS, as well as being merged and minified.
A few application bundler packages are available, including webpack, Parcel, and Gulp. For our use case, we will use webpack. It is the most common module bundler and has a large community surrounding it. To bundle our JavaScript code, we need to install webpack and all its dependencies, as follows:
npm install --save-dev @babel/core babel-loader @babel/preset-env @babel/preset-react clean-webpack-plugin css-loader file-loader html-webpack-plugin style-loader url-loader webpack webpack-cli webpack-dev-server
This command adds all the development tools to devDependencies
in the package.json
file. We will need these to bundle our application. They are only installed in a development environment and are skipped in production.
If you are not already aware, setting up webpack can be a bit of a hassle. Many options can interfere with each other and lead to problems when you're bundling your application. Now, let's create a webpack.client.config.js
file in the root folder of your project.
Enter the following code:
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const buildDirectory = 'dist'; const outputDirectory = buildDirectory + '/client'; module.exports = { mode: 'development', entry: './src/client/index.js', output: { path: path.join(__dirname, outputDirectory), filename: 'bundle.js' }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader' } }, { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] }, devServer: { port: 3000, open: true }, plugins: [ new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: [path.join(__dirname, buildDirectory)] }), new HtmlWebpackPlugin({ template: './public/index.html' }) ] };
The webpack configuration file is just a regular JavaScript file where you can require node_modules
and custom JavaScript files. This is the same as everywhere else inside Node.js. Let's quickly go through all of the main properties of this configuration. Understanding these will make future custom webpack configs much easier. All the important points are explained here:
HtmlWebpackPlugin
: This automatically generates an HTML file that includes all the webpack bundles. We pass our previously createdindex.html
as a template.CleanWebpackPlugin
: This empties all of the provided directories to clean the old build files. ThecleanOnceBeforeBuildPatterns
property specifies an array of folders that are cleaned before the build process is started.- The
entry
field tells webpack where the starting point of our application is. This file needs to be created by us. - The
output
object specifies how our bundle is called and where it should be saved. For us, this isdist/client/bundle.js
. - Inside
module.rules
, we match our file extensions with the correct loaders. All the JavaScript files (except those located innode_modules
) are transpiled by Babel as specified bybabel-loader
so that we can use ES6 features inside our code. Our CSS gets processed bystyle-loader
andcss-loader
. There are many more loaders for JavaScript, CSS, and other file extensions available. - The
devServer
feature of webpack enables us to run the React code directly. This includes hot reloading code in the browser without rerunning a build or refreshing the browser tab.Note
If you need a more detailed overview of the webpack configuration, have a look at the official documentation: https://webpack.js.org/concepts/.
With this in mind, let's move on. We are missing the src/client/index.js
file from our webpack configuration, so let's create it, as follows:
mkdir -p src/client cd src/client touch index.js
You can leave this file empty for the moment. It can be bundled by webpack without content inside. We are going to change it later in this chapter.
To spin up our development webpack server, we will add a command to package.json
that we can run using npm
.
Add the following line to the scripts
object inside package.json
:
"client": "webpack serve --devtool inline-source-map --hot --config webpack.client.config.js"
Now, execute npm run client
in your console and watch how a new browser window opens. We are running webpack serve
with the newly created configuration file.
Sure, the browser is still empty, but if you inspect the HTML with Chrome DevTools, you will see that we have already got a bundle.js
file and that our index.html
file was taken as a template.
With that, we've learned how to include our empty index.js
file with the bundle and serve it to the browser. Next, we'll render our first React component inside our template index.html
file.
Rendering your first React component
There are many best practices for React. The central philosophy behind it is to split our code into separate components where possible. We are going to cover this approach in more detail in Chapter 5, Reusable React Components and React Hooks.
Our index.js
file is the main starting point of our frontend code, and this is how it should stay. Do not include any business logic in this file. Instead, keep it as clean and slim as possible.
The index.js
file should include the following code:
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(<App/>, document.getElementById('root'));
The release of ECMAScript 2015 introduced the import
feature. We can use it to load our npm
packages – react
and react-dom
– and our first custom React component, which we must write now.
Of course, we need to cover the traditional Hello World
program.
Create the App.js
file next to your index.js
file and ensure it contains the following content:
import React from 'react'; const App = () => { return ( <div>Hello World!</div> ) } export default App
Here, we exported a single stateless function called App
that is then imported by the index.js
file. As we explained previously, we are now actively using ReactDOM.render
in our index.js
file.
The first parameter of ReactDOM.render
is the component or function that we want to render, which is the exported function displaying the Hello World! message. The second parameter is the browser's DOMNode
, where it should render. We receive DOMNode
with a plain document.getElementById
JavaScript.
We defined our root element when we created the index.html
file. After saving the App.js
file, webpack will try to build everything again. However, it should not be able to do that. webpack will encounter a problem when bundling our index.js
file because of the <App />
tag syntax we are using in the ReactDOM.render
method. It was not transpiled to a normal JavaScript function.
We configured webpack to load Babel for our JavaScript file but did not tell Babel what to transpile and what not to transpile.
Let's create a .babelrc
file in the root folder that contains the following content:
{ "presets": ["@babel/env","@babel/react"] }
Note
You may have to restart the server because the .babelrc
file is not reloaded when changes are made to the file. After a few moments, you should see the standard Hello World! message in your browser.
Here, we told Babel to use @babel/preset-env
and @babel/preset-react
, which we installed together with webpack. These presets allow Babel to transform specific syntax, such as JSX. We can use those presets to create normal JavaScript that all browsers can understand and that webpack can bundle.
Rendering arrays from React state
Hello World!
is a must for every good programming book, but this is not what we are aiming for when we use React.
A social network such as Facebook or Graphbook, which we are writing now, needs a newsfeed and an input to post news. Let's implement this.
Since this is the first chapter of this book, we will do this inside App.js
.
We should work with some fake data here since we have not set up our GraphQL API yet. We can replace this with real data later.
Define a new variable above the default
exported App
function, like this:
const initialPosts = [ { id: 2, text: 'Lorem ipsum', user: { avatar: '/uploads/avatar1.png', username: 'Test User' } }, { id: 1, text: 'Lorem ipsum', user: { avatar: '/uploads/avatar2.png', username: 'Test User 2' } } ];
We are going to render these two fake posts in React. To prepare this, change the first line of the App.js
file to the following:
import React, { useState } from 'react';
This ensures that the useState
function of React is imported and accessible by our stateless function.
Replace the current content of your App
function with the following code:
const [posts, setPosts] = useState(initialPosts); return ( <div className="container"> <div className="feed"> { initialPosts.map((post, i) => <div key={post.id} className="post"> <div className="header"> <img src={post.user.avatar} /> <h2>{post.user.username}</h2> </div> <p className="content"> {post.text} </p> </div> )} </div> </div> )
Here, we initiated a posts
array inside the function via the React useState
function. It allows us to have a state without writing a real React class; instead, it only relies on raw functions. The useState
function expects one parameter, which is the initial value of the state variable. In this case, this is the constant initialPosts
array. This returns the posts
state variable and a setPosts
function, which you can use to update the local state.
Then, we iterated over the posts
array with the map
function, which again executes the inner callback function, passing each array item as a parameter one by one. The second parameter is just called i
and represents the index of the array element we are processing. Everything that's returned from the map
function is then rendered by React.
We merely returned HTML by putting each post's data in ES6 curly braces. These curly braces tell React to interpret and evaluate the code inside them as JavaScript.
As shown in the preceding code, we are relying on the posts that were returned by the useState
function. This data flow is very convenient because we can update the state at any point in our application and the posts will rerender. The important thing is that this will only work by using the setPosts
function and passing the updated array to it. In this case, React notices the change of state and rerenders the function.
The preceding method is much cleaner, and I recommend this for readability purposes. When saving, you should be able to see rendered posts. They should look like this:
The images I am using here are freely available. You can use any other material that you have got if the path matches the string from the posts
array. You can find those images in the official GitHub repository for this book.
CSS with webpack
The posts in the preceding figure have not been designed yet. I have already added CSS classes to the HTML our component returns.
Instead of using CSS to make our posts look better, another method is to use CSS-in-JS using packages such as styled components, which is a React package. Other alternatives include Glamorous and Radium. There are numerous reasons for and against using such libraries. With those other tools, you are not able to use SASS, SCSS, or LESS effectively. I need to work with other people, such as screen and graphics designers, who can provide and use CSS, but do not program styled components. There is always a prototype or existing CSS that can be used, so why should I spend time translating this into styled components CSS when I could just continue with standard CSS?
There is no right or wrong option here; you are free to implement the styling in any way you like. However, in this book, we will keep using good old CSS.
What we have already done in our webpack.client.config.js
file is specify a CSS rule, as shown in the following code snippet:
{ test: /\.css$/, use: ['style-loader', 'css-loader'], },
style-loader
injects your bundled CSS right into the DOM. css-loader
will resolve all import
or url
occurrences in your CSS code.
Create a style.css
file in /assets/css
and fill in the following:
body { background-color: #f6f7f9; margin: 0; font-family: 'Courier New', Courier, monospace } p { margin-bottom: 0; } .container { max-width: 500px; margin: 70px auto 0 auto; } .feed { background-color: #bbb; padding: 3px; margin-top: 20px; } .post { background-color: #fff; margin: 5px; } .post .header { height: 60px; } .post .header > * { display: inline-block; vertical-align: middle; } .post .header img { width: 50px; height: 50px; margin: 5px; } .post .header h2 { color: #333; font-size: 24px; margin: 0 0 0 5px; } .post p.content { margin: 5px; padding: 5px; min-height: 50px; }
Refreshing your browser will leave you with the same old HTML you had previously.
This problem occurs because webpack is a module bundler and does not know anything about CSS; it only knows JavaScript. We must import the CSS file somewhere in our code.
Instead of using index.html
and adding a head
tag, we can use webpack and our CSS rule to load it right into App.js
. This solution is very convenient since all the required CSS throughout our application gets minified and bundled. Webpack automates this process.
In your App.js
file, add the following behind the React import
statement:
import '../../assets/css/style.css';
webpack magically rebuilds our bundle and refreshes our browser tab.
With that, you have successfully rendered fake data via React and styled it with bundled CSS from webpack. It should look something like this:
The output looks very good already.
Event handling and state updates with React
For this project, it would be great to have a simple textarea
where we can click a button and then add a new post to the static posts
array we wrote in the App
function.
Add the following code above the div
tag that contains the feed
class:
<div className="postForm"> <form onSubmit={handleSubmit}> <textarea value={postContent} onChange={(e) => setPostContent(e.target.value)} placeholder="Write your custom post!"/> <input type="submit" value="Submit" /> </form> </div>
You can use forms in React without any problems. React can intercept the submit event of requests by giving the form an onSubmit
property, which will be a function to handle the logic.
We are passing the postContent
variable to the value
property of textarea
to get what is called a controlled input.
Create an empty string variable to save the textarea
value by using the useState
function from React:
const [postContent, setPostContent] = useState('');
The postContent
variable is already being used for our new textarea
since we specified it in the value
property. Furthermore, we directly implemented the setPostContent
function in our post form. This is used for the onChange
property or any event that is called whenever you type inside textarea
. The setPostContent
function receives the e.target.value
variable, which is the DOM accessor for the value of textarea
, which is then stored in the state of the React function.
Look at your browser again. The form is there, but it is not pretty, so add the following CSS:
form { padding-bottom: 20px; } form textarea { width: calc(100% - 20px); padding: 10px; border-color: #bbb; } form [type=submit] { border: none; background-color: #6ca6fd; color: #fff; padding: 10px; border-radius: 5px; font-size: 14px; float: right; }
The last step is to implement the handleSubmit
function for our form. Add it straight after the state variables and the return
statement:
const handleSubmit = (event) => { event.preventDefault(); const newPost = { id: posts.length + 1, text: postContent, user: { avatar: '/uploads/avatar1.png', username: 'Fake User' } }; setPosts([newPost, ...posts]); setPostContent(''); };
The preceding code looks more complicated than it is, but I am going to explain it quickly.
We needed to run event.preventDefault
to stop our browser from actually trying to submit the form and reload the page. Most people that come from jQuery or other JavaScript frameworks will know this.
Next, we saved our new post in the newPost
variable, which we want to add to our feed.
We faked some data here to simulate a real-world application. For our test case, the new post ID is the number of posts in our state variable plus one. React wants us to give every child in the ReactDOM a unique ID. By counting the number of posts with posts.length
, we simulate the behavior of a real backend giving us unique IDs for our posts.
The text for our new post comes from the postContent
state variable.
Furthermore, we do not have a user system right now that our GraphQL server can use to give us the newest posts, including the matching users and their avatars. We can simulate this by having a static user object for all the new posts we create.
Finally, we updated the state again. We did this by using the setPosts
function and passing a merged array consisting of the new posts and the current posts
array with a destructuring assignment. After that, we cleared textarea
by passing an empty string to the setPostContent
function.
Now, go ahead and play with your working React form. Do not forget that all the posts you create do not persist since they are only held in the local memory of the browser and are not saved to a database. Consequently, refreshing deletes your posts.
Controlling document heads with React Helmet
When developing a web application, you must control your document heads. You might want to change the title or description, based on the content you are presenting.
React Helmet is a great package that offers this on the fly, including overriding multiple headers and server-side rendering. Let's see how we can do this:
- Install React Helmet with the following command:
npm install --save react-helmet
You can add all standard HTML headers with React Helmet.
I recommend keeping standard
head
tags inside your template. This has the advantage that, before React has been rendered, there is always the default document head. For our case, you can directly apply a title and description inApp.js
. - Import
react-helmet
at the top of the file:import { Helmet } from 'react-helmet';
- Add
Helmet
directly abovepostForm div
:<Helmet> <title>Graphbook - Feed</title> <meta name="description" content="Newsfeed of all your friends on Graphbook" /> </Helmet>
If you reload the browser and watch the title on the tab bar of your browser carefully, you will see that it changes from Graphbook
to Graphbook - Feed
. This behavior happens because we already defined a title inside index.html
. When React finishes rendering, the new document head is applied.
Production build with webpack
The last step for our React setup is to have a production build. Until now, we were only using webpack-dev-server
, but this naturally includes an unoptimized development build. Furthermore, webpack automatically spawns a web server. In the next chapter, we will introduce Express.js as our web server so that we don't need webpack to start it.
A production bundle does merge all JavaScript files, but it also merges all the CSS files into two separate files. Those can be used directly in the browser. To bundle CSS files, we will rely on another webpack plugin, called MiniCss
:
npm install --save-dev mini-css-extract-plugin
We do not want to change the current webpack.client.config.js
file because it is made for development work. Add the following command to the scripts
object of your package.json
:
"client:build": "webpack --config webpack.client.build.config.js"
This command runs webpack using an individual production webpack config file. Let's create this one. First, clone the original webpack.client.config.js
file and rename it webpack.client.build.config.js
.
Change the following things in the new file:
mode
needs to beproduction
, notdevelopment
.- Require the
MiniCss
plugin:const MiniCssExtractPlugin = require('mini-css-extract-plugin');
- Replace the current CSS rule:
{ test: /\.css$/, use: [{ loader: MiniCssExtractPlugin.loader, options: { publicPath: '../' } }, 'css-loader'], },
We are no longer using
style-loader
; instead, we're using theMiniCss
plugin. The plugin goes through the complete CSS code, merges it into a separate file, and removes theimport
statements from thebundle.js
file, which we generate in parallel. - Lastly, add the plugin to the plugins at the bottom of the configuration file:
new MiniCssExtractPlugin({ filename: 'bundle.css', })
- Remove the entire
devServer
property.
When you run the new configuration, it won't spawn a server or browser window; it will only create a production JavaScript and CSS bundle and will require them in our index.html
file. According to our webpack.client.build.config.js
file, those three files are going to be saved to the dist/client
folder.
You can run this command by executing npm run client:build
.
If you look in the dist/client
folder, you will see three files. You can open the index.html
file in your browser. Sadly, the images are broken because the image URLs are not correct anymore. We must accept this for the moment because it will be automatically fixed when we have a working backend.
With that, we have finished the basic setup for React.