© Adam Freeman 2019
A. FreemanEssential TypeScripthttps://doi.org/10.1007/978-1-4842-4979-6_20

20. Creating a React App, Part 2

Adam Freeman1 
(1)
London, UK
 
In this chapter, I complete the React web application by adding URL routing and the remaining components before preparing the application for deployment in a container. For quick reference, Table 20-1 lists the TypeScript compiler options used in this chapter.
Table 20-1.

The TypeScript Compiler Options Used in This Chapter

Name

Description

allowJs

This option includes JavaScript files in the compilation process.

allowSyntheticDefaultImports

This option allows imports from modules that do not declare a default export. This option is used to increase code compatibility.

esModuleInterop

This option adds helper code for importing from modules that do not declare a default export and is used in conjunction with the allowSyntheticDefaultImports option.

forceConsistentCasingInFileNames

This option ensures that names in import statements match the case used by the imported file.

isolatedModules

This option treats each file as a separate module, which increases compatibility with the Babel tool.

jsx

This option specifies how HTML elements in TSX files are processed.

lib

This option selects the type declaration files the compiler uses.

module

This option determines the style of module that is used.

moduleResolution

This option specifies the style of module resolution that should be used to resolve dependencies.

noEmit

This option prevents the compiler from emitting JavaScript code, with the result that it only checks code for errors.

resolveJsonModule

This option allows JSON files to be imported as though they were modules.

skipLibCheck

This option speeds up compilation by skipping the normal checking of declaration files.

strict

This option enables stricter checking of TypeScript code.

target

This option specifies the version of the JavaScript language that the compiler will target in its output.

Preparing for This Chapter

In this chapter, I continue to work with the reactapp project started in Chapter 19. Open a command prompt, navigate to the reactapp folder, and run the command shown in Listing 20-1 to start the web service and the React development tools.

Tip

You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/Apress/essential-typescript .

npm start
Listing 20-1.

Starting the Development Tools

After the initial build process, a new browser window will open and display the example application, as shown in Figure 20-1.
../images/481342_1_En_20_Chapter/481342_1_En_20_Fig1_HTML.jpg
Figure 20-1.

Running the example application

Configuring URL Routing

Most real React projects rely on URL routing, which uses the browser’s current URL to select the components that are displayed to the user. React doesn’t include built-in support for URL routing, but the most commonly used package is React Router. Open a new command prompt, navigate to the reactapp folder, and run the commands shown in Listing 20-2 to install the React Router package and the type definition files.
npm install [email protected]
npm install --save-dev @types/react-router-dom
Listing 20-2.

Adding a Package to the Project

The React Router package supports different navigation systems, and the react-router-dom package contains the functionality required for web applications. Table 20-2 shows the URLs that the example application will support and the purpose of each of them.
Table 20-2.

The URLs Supported by the Application

Name

Description

/products

This URL will display the ProductList component defined in Chapter 19.

/order

This URL will display a component that displays details of the order.

/summary

This URL will display a summary of an order once it has been sent to the server. The URL will include the number assigned to the order so that an order whose ID is 5 will be displayed using the URL /summary/5.

/

The default URL will be redirected to /products so the ProductList component is shown.

Not all the components required by the application have been written, so Listing 20-3 sets up the configuration for the /products and / URLs, with the others to be defined in the sections that follow.
import React, { Component } from 'react';
import { dataStore } from "./data/dataStore";
import { Provider } from 'react-redux';
import { HttpHandler } from "./data/httpHandler";
import { addProduct } from './data/actionCreators';
import { ConnectedProductList } from './data/productListConnector';
import { Switch, Route, Redirect, BrowserRouter } from "react-router-dom";
interface Props {
    // no props required
}
export default class App extends Component<Props> {
    private httpHandler = new HttpHandler();
    constructor(props: Props) {
        super(props);
        this.httpHandler.loadProducts(data =>
            dataStore.dispatch(addProduct(...data)));
    }
    render = () =>
        <div className="App">
            <Provider store={ dataStore }>
                <BrowserRouter>
                    <Switch>
                        <Route path="/products" component={ ConnectedProductList } />
                        <Redirect to="/products" />
                    </Switch>
                </BrowserRouter>
            </Provider>
        </div>
}
Listing 20-3.

Configuring URL Routing in the App.tsx File in the src Folder

The React Router package relies on components for configuration. The BrowserRouter component defines a region of content that is selected by using the browser’s URL. The Route component creates a mapping between a URL and a component. The Switch component is equivalent to a JavaScript switch block and selects the component from the first Route component whose path prop matches the current URL. The Redirect component provides a fallback that directs the browser to a URL if there are no other matches. When the changes in Listing 20-3 are saved, the application will be rebuilt, and the browser will be redirected to the /products URL, as shown in Figure 20-2.
../images/481342_1_En_20_Chapter/481342_1_En_20_Fig2_HTML.jpg
Figure 20-2.

Adding URL routing

Completing the Example Application Features

Now that the application can display components based on the current URL, I can add the remaining components to the project. To enable URL navigation from the button displayed by the Header component, I added the statements shown in Listing 20-4 to the header.tsx file.
import React, { Component } from "react";
import { Order } from "./data/entities";
import { NavLink } from "react-router-dom";
interface Props {
    order: Order
}
export class Header extends Component<Props> {
    render() {
        let count = this.props.order.productCount;
        return <div className="p-1 bg-secondary text-white text-right">
            { count === 0 ? "(No Selection)"
                : `${ count } product(s), $${ this.props.order.total.toFixed(2)}` }
            <NavLink to="/order" className="btn btn-sm btn-primary m-1">
                Submit Order
            </NavLink>
        </div>
    }
}
Listing 20-4.

Adding Navigation in the header.tsx File in the src Folder

The NavLink component produces an anchor element (an element whose tag is a) that navigates to a specified URL when it is clicked. The Bootstrap classes applied to the NavLink give the link the appearance of a button.

Adding the Order Summary Component

To display the details of the order to the user, add a file called orderDetails.tsx to the src folder and add the code shown in Listing 20-5.
import React, { Component } from "react";
import { StoreData } from "./data/types";
import { Order } from "./data/entities";
import { connect } from "react-redux";
import { NavLink } from "react-router-dom";
const mapStateToProps = (data: StoreData) => ({
    order: data.order
})
interface Props {
    order: Order,
    submitCallback: () => void
}
const connectFunction = connect(mapStateToProps);
export const OrderDetails = connectFunction(
    class extends Component<Props> {
        render() {
            return <div>
            <h3 className="text-center bg-primary text-white p-2">Order Summary</h3>
            <div className="p-3">
                <table className="table table-sm table-striped">
                    <thead>
                        <tr>
                            <th>Quantity</th><th>Product</th>
                            <th className="text-right">Price</th>
                            <th className="text-right">Subtotal</th>
                        </tr>
                    </thead>
                    <tbody>
                        { this.props.order.orderLines.map(line =>
                            <tr key={ line.product.id }>
                                <td>{ line.quantity }</td>
                                <td>{ line.product.name }</td>
                                <td className="text-right">
                                    ${ line.product.price.toFixed(2) }
                                </td>
                                <td className="text-right">
                                    ${ line.total.toFixed(2) }
                                </td>
                            </tr>
                        )}
                    </tbody>
                    <tfoot>
                        <tr>
                            <th className="text-right" colSpan={3}>Total:</th>
                            <th className="text-right">
                                ${ this.props.order.total.toFixed(2) }
                            </th>
                        </tr>
                    </tfoot>
                </table>
            </div>
            <div className="text-center">
                <NavLink to="/products" className="btn btn-secondary m-1">
                    Back
                </NavLink>
                <button className="btn btn-primary m-1"
                        onClick={ this.props.submitCallback }>
                    Submit Order
                </button>
            </div>
        </div>
    }});
Listing 20-5.

The Contents of the orderDetails.tsx File in the src Folder

In Chapter 19, I created a connector for an existing component so that it would receive props that are linked to the data store. In Listing 20-5, I have created a component that is always connected to the data store, which avoids the need to define a separate connector but does mean that the component can’t be used when the data store isn’t available, such as in another project. This component uses a NavLink to return the user to the /products button and invokes a function prop when the user is ready to send the order to the web service.

Adding the Confirmation Component

Add a file named summary.tsx to the src folder and add the code shown in Listing 20-6 to display a message to the user once the order has been stored by the web service.
import React, { Component } from "react";
import { match } from "react-router";
import { NavLink } from "react-router-dom";
interface Params {
    id: string;
}
interface Props {
    match: match<Params>
}
export class Summary extends Component<Props> {
    render() {
        let id = this.props.match.params.id;
        return <div className="m-2 text-center">
            <h2>Thanks!</h2>
            <p>Thanks for placing your order.</p>
            <p>Your order is #{ id }</p>
            <p>We'll ship your goods as soon as possible.</p>
            <NavLink to="/products" className="btn btn-primary">OK</NavLink>
        </div>
    }
}
Listing 20-6.

The Contents of the summary.tsx File in the src Folder

The Summary component only needs to know the number assigned by the web service to the user’s order, which it obtains from the current route. The routing package provides details of the route through props, following the established React pattern. The type declarations for the React Router package are used to describe the parameter that the component expects, allowing the TypeScript compiler to check types.

Completing the Routing Configuration

In Listing 20-7, I added new Route elements to display the OrderDetails and Summary components, completing the routing configuration for the example application.
import React, { Component } from 'react';
import { dataStore } from "./data/dataStore";
import { Provider } from 'react-redux';
import { HttpHandler } from "./data/httpHandler";
import { addProduct } from './data/actionCreators';
import { ConnectedProductList } from './data/productListConnector';
import { Switch, Route, Redirect, BrowserRouter, RouteComponentProps }
    from "react-router-dom";
import { OrderDetails } from './orderDetails';
import { Summary } from './summary';
interface Props {
    // no props required
}
export default class App extends Component<Props> {
    private httpHandler = new HttpHandler();
    constructor(props: Props) {
        super(props);
        this.httpHandler.loadProducts(data =>
            dataStore.dispatch(addProduct(...data)));
    }
    render = () =>
        <div className="App">
            <Provider store={ dataStore }>
                <BrowserRouter>
                    <Switch>
                        <Route path="/products" component={ ConnectedProductList } />
                        <Route path="/order" render={ (props) =>
                            <OrderDetails { ...props } submitCallback={ () =>
                                this.submitCallback(props) } />
                        } />
                        <Route path="/summary/:id" component={ Summary } />
                        <Redirect to="/products" />
                    </Switch>
                </BrowserRouter>
            </Provider>
        </div>
    submitCallback = (routeProps: RouteComponentProps) => {
        this.httpHandler.storeOrder(dataStore.getState().order,
            id => routeProps.history.push( `/summary/${id}`));
    }
}
Listing 20-7.

Adding the Remaining Routes in the App.tsx File in the src Folder

The Route component for the OrderDetails component uses the render function to select the component and provide it with a mix of props provided by the routing system and a callback function. The submitCallback method requires access to the routing features that are provided as props to components in order to navigate to a new URL, but these are available only within the Browser router component. To work around this limitation, I provide the OrderDetails component with an inline function that passes the routing props to the submitCallback method, which allows the history.push method to be used. The Route component for the Summary component defines a URL with a parameter that provides the order number to display to the user.

When the changes are saved, items can be added to the order, and the order can be sent to the web service, as shown in Figure 20-3.
../images/481342_1_En_20_Chapter/481342_1_En_20_Fig3_HTML.jpg
Figure 20-3.

Completing the example application

Deploying the Application

The React development tools rely on the Webpack Development Server, which is not suitable for hosting a production application because it adds features such as automatic reloading to the JavaScript bundles it generates. In this section, I work through the process of preparing the application for deployment, which is a similar process for any web application, including those developed using other frameworks.

Adding the Production HTTP Server Package

For production, a regular HTTP server is required to deliver the HTML, CSS, and JavaScript files to the browser. For this example, I am going to use the Express server, which is the same package I use for the other examples in this part of the book and which is a good choice for any web application. Use Control+C to stop the development tools and use the command prompt to run the command shown in Listing 20-8 in the reactapp folder to install the express package.

The second command installs the connect-history-api-fallback package, which is useful when deploying applications that use URL routing because it maps requests for the URLs that the application supports to the index.html file, ensuring that reloading the browser doesn’t present the user with a “not found” error.
npm install --save-dev [email protected]
npm install --save-dev [email protected]
Listing 20-8.

Adding Packages for Deployment

Creating the Persistent Data File

To create the persistent data file for the web service, add a file called data.json to the reactapp folder and add the content shown in Listing 20-9.
{
    "products": [
        { "id": 1, "name": "Kayak", "category": "Watersports",
            "description": "A boat for one person", "price": 275 },
        { "id": 2, "name": "Lifejacket", "category": "Watersports",
            "description": "Protective and fashionable", "price": 48.95 },
        { "id": 3, "name": "Soccer Ball", "category": "Soccer",
            "description": "FIFA-approved size and weight", "price": 19.50 },
        { "id": 4, "name": "Corner Flags", "category": "Soccer",
            "description": "Give your playing field a professional touch",
            "price": 34.95 },
        { "id": 5, "name": "Stadium", "category": "Soccer",
            "description": "Flat-packed 35,000-seat stadium", "price": 79500 },
        { "id": 6, "name": "Thinking Cap", "category": "Chess",
            "description": "Improve brain efficiency by 75%", "price": 16 },
        { "id": 7, "name": "Unsteady Chair", "category": "Chess",
            "description": "Secretly give your opponent a disadvantage",
            "price": 29.95 },
        { "id": 8, "name": "Human Chess Board", "category": "Chess",
            "description": "A fun game for the family", "price": 75 },
        { "id": 9, "name": "Bling Bling King", "category": "Chess",
            "description": "Gold-plated, diamond-studded King", "price": 1200 }
    ],
    "orders": []
}
Listing 20-9.

The Contents of the data.json File in the reactapp Folder

Creating the Server

To create the server that will deliver the application and its data to the browser, create a file called server.js in the reactapp folder and add the code shown in Listing 20-10.
const express = require("express");
const jsonServer = require("json-server");
const history = require("connect-history-api-fallback");
const app = express();
app.use(history());
app.use("/", express.static("build"));
const router = jsonServer.router("data.json");
app.use(jsonServer.bodyParser)
app.use("/api", (req, resp, next) => router(req, resp, next));
const port = process.argv[3] || 4002;
app.listen(port, () => console.log(`Running on port ${port}`));
Listing 20-10.

The Contents of the server.js File in the reactapp Folder

The statements in the server.js file configure the express and json-server packages so they use the contents of the build folder, which is where the React build process will put the application’s JavaScript bundles and the HTML file that tells the browser to load them. URLs prefixed with /api will be handled by the web service.

Using Relative URLs for Data Requests

The web service that provided the application with data has been running alongside the React development server. To prepare for sending requests to a single port, I changed the HttpHandler class, as shown in Listing 20-11.
import Axios from "axios";
import { Product, Order}  from "./entities";
// const protocol = document.location.protocol;
// const hostname = document.location.hostname;
// const port = 4600;
const urls = {
    // products: `${protocol}//${hostname}:${port}/products`,
    // orders: `${protocol}//${hostname}:${port}/orders`
    products: "/api/products",
    orders: "/api/orders"
};
export class HttpHandler {
    loadProducts(callback: (products: Product[]) => void): void {
        Axios.get(urls.products).then(response => callback(response.data))
    }
    storeOrder(order: Order, callback: (id: number) => void): void {
        let orderData = {
            lines: [...order.orderLines.values()].map(ol => ({
                productId: ol.product.id,
                productName: ol.product.name,
                quantity: ol.quantity
            }))
        }
        Axios.post(urls.orders, orderData)
            .then(response => callback(response.data.id));
    }
}
Listing 20-11.

Using Relative URLs in the httpHandler.ts File in the src/data Folder

The URLs in Listing 20-11 are specified relative to the one used to request the HTML document, following the common convention that data requests are prefixed with /api.

Building the Application

Run the command shown in Listing 20-12 in the reactapp folder to create the production build of the application.
npm run build
Listing 20-12.

Creating the Production Bundle

The build process creates a set of optimized files in the build folder. The build process can take a few moments to complete and will produce the following output, which shows which files have been created:
Creating an optimized production build...
=============
Compiled successfully. File sizes after gzip:
  54.12 KB         buildstaticjs2.cbc9ddf3.chunk.js
  22.17 KB         buildstaticcss2.22a7d4ef.chunk.css
  2.47 KB (-28 B)  buildstaticjsmain.e2264e9c.chunk.js
  762 B            buildstaticjs untime~main.a8a9905a.js
  269 B            buildstaticcssmain.5ecd60fb.chunk.css
The project was built assuming it is hosted at the server root.
You can control this with the homepage field in your package.json.
For example, add this to build it for GitHub Pages:
  "homepage" : "http://myname.github.io/myapp",
The build folder is ready to be deployed.
You may serve it with a static server:
  npm install -g serve
  serve -s build
Find out more about deployment here:
  https://bit.ly/CRA-deploy

Testing the Production Build

To make sure that the build process has worked and the configuration changes have taken effect, run the command shown in Listing 20-13 in the reactapp folder.
node server.js
Listing 20-13.

Starting the Production Server

The code from Listing 20-13 will be executed and will produce the following output:
Running on port 4002
Open a new web browser and navigate to http://localhost:4002, which will show the application, as illustrated in Figure 20-4.
../images/481342_1_En_20_Chapter/481342_1_En_20_Fig4_HTML.jpg
Figure 20-4.

Running the production build

Containerizing the Application

To complete this chapter, I am going to create a Docker container for the example application so that it can be deployed into production. If you did not install Docker in Chapter 15, then you must do so now to follow the rest of the examples in this chapter.

Preparing the Application

The first step is to create a configuration file for NPM that will be used to download the additional packages required by the application for use in the container. I created a file called deploy-package.json in the reactapp folder with the content shown in Listing 20-14.
{
    "name": "reactapp",
    "description": "React Web App",
    "repository": "https://github.com/Apress/essential-typescript",
    "license": "0BSD",
    "devDependencies": {
        "express": "4.16.4",
        "json-server": "0.14.2",
        "connect-history-api-fallback": "1.6.0"
     }
}
Listing 20-14.

The Contents of the deploy-package.json File in the reactapp Folder

The devDependencies section specifies the packages required to run the application in the container. All of the packages for which there are import statements in the application’s code files will have been incorporated into the bundle created by webpack and are listed. The other fields describe the application, and their main use is to prevent warnings when the container is created.

Creating the Docker Container

To define the container, I added a file called Dockerfile (with no extension) to the reactapp folder and added the content shown in Listing 20-15.
FROM node:12.0.0
RUN mkdir -p /usr/src/reactapp
COPY build /usr/src/reactapp/build/
COPY data.json /usr/src/reactapp/
COPY server.js /usr/src/reactapp/
COPY deploy-package.json /usr/src/reactapp/package.json
WORKDIR /usr/src/reactapp
RUN echo 'package-lock=false' >> .npmrc
RUN npm install
EXPOSE 4002
CMD ["node", "server.js"]
Listing 20-15.

The Contents of the Dockerfile File in the reactapp Folder

The contents of the Dockerfile use a base image that has been configured with Node.js and that copies the files required to run the application into the container, along with the file that lists the packages required for deployment.

To speed up the containerization process, I created a file called .dockerignore in the reactapp folder with the content shown in Listing 20-16. This tells Docker to ignore the node_modules folder, which is not required in the container and takes a long time to process.
node_modules
Listing 20-16.

The Contents of the .dockerignore File in the reactapp Folder

Run the command shown in Listing 20-17 in the reactapp folder to create an image that will contain the example application, along with all the packages it requires.
docker build . -t reactapp -f  Dockerfile
Listing 20-17.

Building the Docker Image

An image is a template for containers. As Docker processes the instructions in the Docker file, the NPM packages will be downloaded and installed, and the configuration and code files will be copied into the image.

Running the Application

Once the image has been created, create and start a new container using the command shown in Listing 20-18.
docker run -p 4002:4002 reactapp
Listing 20-18.

Starting the Docker Container

You can test the application by opening http://localhost:4002 in the browser, which will display the response provided by the web server running in the container, as shown in Figure 20-5.
../images/481342_1_En_20_Chapter/481342_1_En_20_Fig5_HTML.jpg
Figure 20-5.

Running the containerized application

To stop the container, run the command shown in Listing 20-19.
docker ps
Listing 20-19.

Listing the Containers

You will see a list of running containers, like this (I have omitted some fields for brevity):
CONTAINER ID        IMAGE               COMMAND             CREATED
1905eeee03d8        reactapp           "node server.js"     51 seconds ago
Using the value in the Container ID column, run the command shown in Listing 20-20.
docker stop 1905eeee03d8
Listing 20-20.

Stopping the Container

The React application is ready to deploy to any platform that supports Docker.

Summary

In this chapter, I completed the React application by adding support for URL routing and by defining the remaining components. As with the other examples in this part of the book, I prepared the application for deployment and created a Docker image that can be readily deployed. In the next chapter, I create the same web application using Vue.js and TypeScript.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.84.155