Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
ASP.NET Core 5 and React

You're reading from   ASP.NET Core 5 and React Full-stack web development using .NET 5, React 17, and TypeScript 4

Arrow left icon
Product type Paperback
Published in Jan 2021
Publisher Packt
ISBN-13 9781800206168
Length 568 pages
Edition 2nd Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Carl Rippon Carl Rippon
Author Profile Icon Carl Rippon
Carl Rippon
Arrow right icon
View More author details
Toc

Table of Contents (21) Chapters Close

Preface 1. Section 1: Getting Started
2. Chapter 1: Understanding the ASP.NET 5 React Template FREE CHAPTER 3. Chapter 2: Creating Decoupled React and ASP.NET 5 Apps 4. Section 2: Building a Frontend with React and TypeScript
5. Chapter 3: Getting Started with React and TypeScript 6. Chapter 4: Styling React Components with Emotion 7. Chapter 5: Routing with React Router 8. Chapter 6: Working with Forms 9. Chapter 7: Managing State with Redux 10. Section 3: Building an ASP.NET Backend
11. Chapter 8: Interacting with the Database with Dapper 12. Chapter 9: Creating REST API Endpoints 13. Chapter 10: Improving Performance and Scalability 14. Chapter 11: Securing the Backend 15. Chapter 12: Interacting with RESTful APIs 16. Section 4: Moving into Production
17. Chapter 13: Adding Automated Tests 18. Chapter 14: Configuring and Deploying to Azure 19. Chapter 15: Implementing CI and CD with Azure DevOps 20. Other Books You May Enjoy

Understanding the React frontend

It's time to turn our attention to the React frontend. In this section, we'll inspect the frontend code, starting with the entry point, which is a single HTML page. We will explore how the frontend is executed in development mode and how it is built in preparation for deployment. We will then learn how the frontend dependencies are managed and also understand why it took over a minute to run the app for the first time. Finally, we will explore how React components fit together and how they access the ASP.NET Core backend.

Understanding the frontend entry point

We have a good clue as to where the entry point is from our examination of the Startup class in the ASP.NET Core backend. In the Configure method, the SPA middleware is set up with the source path set to ClientApp:

app.UseSpa(spa =>
{
  spa.Options.SourcePath = "ClientApp";
  if (env.IsDevelopment())
  {
    spa.UseReactDevelopmentServer(npmScript: "start");
  }
});

If we look in the ClientApp folder, we'll see a file called package.json. This is a file that is often used in React apps and contains information about the project, its npm dependencies, and the scripts that can be run to perform tasks. 

Important Note

npm is a popular package manager for JavaScript. The dependencies in package.json reference the packages in the npm registry.

If we open the package.json file, we will see react listed as a dependency:

"dependencies": {
  "react": "^16.0.0",
  ...
  "react-scripts": "^3.4.1",
  ...
},

A version is specified against each package name. The versions in your package.json file may be different to the ones shown in the preceding code snippet. The ^ symbol in front of the version means that the latest minor version can be safely installed, according to semantic versioning.

Important Note

A semantic version has three parts: Major.Minor.Patch. A major version increment happens when an API breaking change is made. A minor version increment happens when backward-compatible features are added. Finally, a patch version happens when backward-compatible bug fixes are added. More information can be found at https://semver.org.

So, react 16.14.0 can be safely installed because this is the latest minor version of React 16 at the time of writing this book.

The react-scripts dependency gives us a big clue as to how React was scaffolded. react-scripts is a set of scripts from the popular Create React App (CRA) tool that was built by the developers at Facebook. This tool has done a huge amount of configuration for us, including creating a development server, bundling, linting, and unit testing. We'll learn more about CRA in the next chapter.

The root HTML page for an app scaffolded by CRA is index.html, which can be found in the public folder in the ClientApp folder. It is this page that hosts the React app. The root JavaScript file that is executed for an app scaffolded by CRA is index.js, which is in the ClientApp folder. We'll examine both the index.html and index.js files later in this chapter. 

Next, we will learn how the React frontend is executed in development mode.

Running in development mode

In the following steps, we'll examine the ASP.NET Core project file to see what happens when the app runs in development mode:

  1. We can open the project file by right-clicking on the web application project in Solution Explorer and selecting the Edit Project File option:
    Figure 1.10 – Opening the project file in Visual Studio

    Figure 1.10 – Opening the project file in Visual Studio

    This is an XML file that contains information about the Visual Studio project.

  2. Let's look at the Target element, which has a Name attribute of DebugEnsureNodeEnv:
    <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
      <!-- Ensure Node.js is installed -->
      <Exec Command="node --version" 
        ContinueOnError="true">
        <Output TaskParameter="ExitCode" 
          PropertyName="ErrorCode" />
      </Exec>
      <Error Condition="'$(ErrorCode)' != '0'" 
       Text="Node.js is required to build and run this 
        project. To continue, please install Node.js from 
         https://nodejs.org/, and then restart your
           command prompt or IDE." 
       />
      <Message Importance="high" Text="Restoring 
       dependencies using 'npm'. 
        This may take several minutes..." />
      <Exec WorkingDirectory="$(SpaRoot)" Command="npm 
        install" />
    </Target>

    This executes tasks when the ClientApp/node-modules folder doesn't exist and the Visual Studio project is run in debug mode, which is the mode that's used when we press F5.

  3. The first task that is run in the Target element is the execution of the following command via an Exec task:
    > node --version

    This command returns the version of Node that is installed. This may seem like an odd thing to do, but its purpose is to determine whether node is installed. If Node is not installed, the command will error and be caught by the Error task, which informs the user that Node needs to the installed and where to install it from.

  4. The next task in the Target element uses a Message command, which outputs Restoring dependencies using 'npm'. This may take several minutes... to the Output window. We'll see this message when we run the project for the first time:
    Figure 1.11 – Restoring npm dependencies message when running a project for the first time

    Figure 1.11 – Restoring npm dependencies message when running a project for the first time

  5. The final task that is carried out when the project is run in debug mode is another Exec task. This executes the following npm command:
    > npm install

This command downloads all the packages that are listed as dependencies in package.json into a folder called node_modules:

Figure 1.12 – The node_modules folder

Figure 1.12 – The node_modules folder

We can see this in the Solution Explorer window if the Show All Files option is on. Notice that there are a lot more folders in node_modules than dependencies listed in package.json. This is because the dependencies will have dependencies. So, the packages in node_modules are all the dependencies in the dependency tree.

At the start of this section, we asked ourselves why it took such a long time for the project to run the app for the first time. The answer is that this last task takes a while because there are a lot of dependencies to download and install. On subsequent runs, node_modules will have been created, so these sets of tasks won't get invoked.

Earlier in this chapter, we learned that ASP.NET Core invokes an npm start command when the app is in development mode. If we look at the scripts section in package.json, we'll see the definition of this command:

"scripts": {
  "start": "rimraf ./build && react-scripts start",
  ...
}

This command deletes a folder called build and runs a Webpack development server.

Important Note

Webpack is a tool that transforms, bundles, and packages up files for use in a browser. Webpack also has a development server. The CRA tool has configured Webpack for us so that all the transformation and bundling configuration is already set up for us.

Why would we want to use the Webpack development server when we already have our ASP.NET Core backend running in IIS Express? The answer is a shortened feedback loop, which will increase our productivity. Later, we'll see that we can make a change to a React app running in the Webpack development server and that those changes are automatically loaded. There is no stopping and restarting the application, so there's a really quick feedback loop and great productivity.

Publishing process

The publishing process is the process of building artifacts to run an application in a production environment. 

Let's continue and inspect the XML ASP.NET Core project file by looking at the Target element, which has a Name attribute of PublishRunWebPack. The following code executes a set of tasks when the Visual Studio project is published:

<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
  <!-- As part of publishing, ensure the JS resources are 
  freshly built in production mode -->
   
  <Exec WorkingDirectory="$(SpaRoot)" Command="npm install"
   />
  <Exec WorkingDirectory="$(SpaRoot)" Command="npm run 
   build" />
  <!-- Include the newly-built files in the publish output -->
  <ItemGroup>
    <DistFiles Include="$(SpaRoot)build\**" />
    <ResolvedFileToPublish Include="@(DistFiles-
     >'%(FullPath)')" 
     Exclude="@(ResolvedFileToPublish)">
      <RelativePath>%(DistFiles.Identity)</RelativePath>
      <CopyToPublishDirectory>PreserveNewest
        </CopyToPublishDirectory>
    </ResolvedFileToPublish>
  </ItemGroup>
</Target>

The first task that is run is the execution of the npm install command via an Exec task. This will ensure that all the dependencies are downloaded and installed. Obviously, if we've already run our project in debug mode, then the dependencies should already be in place.

The next task is an Exec task that runs the following npm command:

> npm run build

This task will run an npm script called build. If we look in the package.json file again, we'll see this script in the scripts section: 

"scripts": {
  "start": "rimraf ./build && react-scripts start",
  "build": "react-scripts build",
  "test": "cross-env CI=true react-scripts test --
   env=jsdom",
  "eject": "react-scripts eject",
  "lint": "eslint ./src/"
}

This references the create-react-app scripts, which bundle the React app ready for production, optimizing it for great performance, and outputting the content into a folder called build

The next set of tasks defined in the ItemGroup element take their content from the build folder and place it in the publish location, along with the rest of the content to publish.

Let's give this a try and publish our app:

  1. In the Solution Explorer window, right-click on the project and select the Publish... option.
  2. Choose Folder as the target and click Next:
    Figure 1.13 – Publishing to a folder

    Figure 1.13 – Publishing to a folder

  3. Enter a folder location to output the content to and click Finish:
    Figure 1.14 – Publish location

    Figure 1.14 – Publish location

  4. A publish profile is then created. Click the Publish button to start the publishing process on the screen that appears:
Figure 1.15 – Publish profile screen

Figure 1.15 – Publish profile screen

After a while, we'll see the content appear in the folder we specified, including a ClientApp folder. If we look in this ClientApp folder, we'll see a build folder containing the React app, ready to be run in a production environment. Notice that the build folder contains index.html, which is the single page that will host the React app in production.

Important Note

It is important to note that publishing from a developer's machine is not ideal. Instead, it is good practice to carry out this process on a build server to make sure that built applications are consistent, and that code that's committed to the repository goes into the build. We'll cover this in Chapter 15Implementing CI and CD with Azure DevOps.

Understanding the frontend dependencies

Earlier, we learned that frontend dependencies are defined in package.json. Why not just list all the dependencies as script tags in index.html? Why do we need the extra complexity of npm package management in our project? The answer is that a long list of dependencies is hard to manage. If we used script tags, we'd need to make sure these are ordered correctly. We'd also be responsible for downloading the packages, placing them locally in our project, and keeping them up to date. We have a huge list of dependencies in our scaffolded project already, without starting work on any functionality in our app. For these reasons, managing dependencies with npm has become an industry standard.

Let's open package.json again and look at the dependencies section:

"dependencies": {
  "bootstrap": "^4.1.3",
  "jquery": "3.4.1",
  "merge": "^1.2.1",
  "oidc-client": "^1.9.0", 
  "react": "^16.0.0",
  "react-dom": "^16.0.0",
  "react-router-bootstrap": "^0.24.4",
  "react-router-dom": "^4.2.2",
  "react-scripts": "^3.0.1",
  "reactstrap": "^6.3.0",
  "rimraf": "^2.6.2"
},

We've already observed the react dependency, but what is the react-dom dependency? Well, React doesn't just target the web; it also targets native mobile apps. This means that react is the core React library that is used for both web and mobile, and react-dom is the library that's specified for targeting the web.

The react-router-dom package is the npm package for React Router and helps us manage the different pages in our app in the React frontend, without us needing to do a round trip to the server. We'll learn more about React Router in Chapter 5Routing with React Router. The react-router-bootstrap package allows Bootstrap to work nicely with React Router.

We can see that this React app has a dependency for Bootstrap 4.1 with the bootstrap npm package. So, Bootstrap CSS classes and components can be referenced to build the frontend in our project. The reactstrap package is an additional package that allows us to consume Bootstrap nicely in React apps. Bootstrap 4.1 has a dependency on jQuery, which is the reason why we have the jquery package dependency. 

The merge package contains a function that merges objects together, while oidc-client is a package for interacting with OpenID Connect (OIDC) and OAuth2.

The final dependency that we haven't covered yet is rimraf. This simply allows files to be deleted, regardless of the host operating system. We can see that this is referenced in the start script:

"scripts": {
  "start": "rimraf ./build && react-scripts start",
  ...
}

Earlier in this chapter, we learned that this script is invoked when our app is running in development mode. So, rimraf ./build deletes the build folder and its contents before the development server starts.

If we look further down, we'll see a section called devDependencies. These are dependencies that are only used during development and not in production:

"devDependencies": {
  "ajv": "^6.9.1",
  "cross-env": "^5.2.0",
  "eslint": "^6.8.0",
  "eslint-config-react-app": "^5.2.1",
  "eslint-plugin-flowtype": "^4.6.0",
  "eslint-plugin-import": "^2.20.0",
  "eslint-plugin-jsx-a11y": "^6.2.3",
  "eslint-plugin-react": "^7.18.3"
},

The following is a brief description of these dependencies:

  • ajv allows us to validate JSON files.
  • cross-env allows us to set environment variables, regardless of the host operating system. If you look at the test script in the scripts section of the package.json file, you'll see that it uses cross-env to set a CI environment variable.
  • The remaining dependencies are all designed to enable linting with ESLint. The linting process checks for problematic patterns in code according to a set of rules. We'll learn more about ESLint in Chapter 3Getting Started with React and TypeScript.

Let's move on and learn how the single page is served and how the React app is injected into it.

Understanding how the single page is served

We know that the single page that hosts the React app is index.html, so let's examine this file. This file can be found in the public folder of the ClientApp folder. The React app will be injected into the div tag, which has an id of root:

<div id="root"></div>

Let's run our app again in Visual Studio to confirm that this is the case by pressing F5. If we open the developer tools in the browser page that opens and inspect the DOM in the Elements panel, we'll see this div tag with the React content inside it:

Figure 1.16 – Root div element and script elements

Figure 1.16 – Root div element and script elements

Notice the script elements at the bottom of the body element. This contains all the JavaScript code for our React app, including the React library itself. However, these script elements don't exist in the source index.html file, so how did they get there in the served page? Webpack added them after bundling all the JavaScript together and splitting it up into optimal chunks that can be loaded on demand. If we look in the ClientApp folder and subfolders, we'll see that the static folder doesn't exist. The JavaScript files don't exist either. What's going on? These are virtual files that are created by the Webpack development server. Remember that when we run the app with Visual Studio debugger, the Webpack development server serves index.html. So, the JavaScript files are virtual files that the Webpack development server creates.

Now, what happens in production mode when the Webpack development server isn't running? Let's have a closer look at the app we published earlier in this chapter. Let's look in the index.html file in the Build folder, which can be found in the ClientApp folder. The script elements at the bottom of the body element will look something like the following: 

<script>
  !function(e){...}([])
</script>
<script src="/static/js/2.f6873cc5.chunk.js"></script>
<script src="/static/js/main.61537c83.chunk.js"></script>

Carriage returns have been added in the preceding code snippet to make it more readable. The highlighted parts of the filenames may vary each time the app is published. The filenames are unique in order to break browser caching. If we look for these JavaScript files in our project, we'll find that they do exist. So, in production mode, the web server will serve this physical JavaScript file.

If we open this JavaScript file, we'll see it contains all the JavaScript for our app. This JavaScript is minified so that the file can be downloaded to the browser nice and quickly. 

Important Note

Minification is the process of removing unnecessary characters in files without affecting how it is processed by the browser. This includes code comments and formatting, unused code, using shorter variable and function names, and so on. 

However, the file isn't small and contains a lot of JavaScript. What's going on here? Well, the file contains not only our JavaScript app code but also the code from all the dependencies, including React itself.

Understanding how components fit together

Now, it's time to start looking at the React app code and how components are implemented. Remember that the root JavaScript file is index.js in the ClientApp folder. Let's open this file and look closely at the following block of code:

const rootElement = document.getElementById('root');
ReactDOM.render(
  <BrowserRouter basename={baseUrl}>
    <App />
  </BrowserRouter>,
  rootElement);

The first statement selects the div element we discovered earlier, which contains the root ID and stores it in a variable called rootElement.

The next statement extends over multiple lines and calls the render function from the React DOM library. It is this function that injects the React app content into the root div element. The rootElement variable, which contains a reference to the root div element, is passed into this function as the second parameter.

The first parameter that is passed into the render function is more interesting. In fact, it doesn't even look like legal JavaScript! This is, in fact, JSX, which we'll learn about in detail in Chapter 3Getting Started with React and TypeScript.

Important Note

JSX is transformed into regular JavaScript by Webpack using a tool called Babel. This is one of many tasks that CRA configured for us when our app was scaffolded.

So, the first parameter passes in the root React component called BrowserRouter, which comes from the React Router library. We'll learn more about this component in Chapter 5Routing with React Router.

Nested inside the BrowserRouter component is a component called App. If we look at the top of the index.js file, we will see that the App component is imported from a file called App.js:

import App from './App';

Important Note

import statements are used to import items that have been exported by another JavaScript module. The module is specified by its file location, with the js extension omitted. 

The import statements that import items from npm packages don't need the path to be specified. This is because CRA has configured a resolver in Webpack that automatically looks in the node_modules folder during the bundling process.

So, the App component is contained in the App.js file. Let's have a quick look. A class called App is defined in this file: 

export default class App extends Component {
  static displayName = App.name;
  render () {
    return (
      <Layout>
        <Route exact path='/' component={Home} />
        <Route path='/counter' component={Counter} />
        <Route path='/fetch-data' component={FetchData} />
      </Layout>
    );
  }
}

Notice the export and default keywords before the class keyword.

Important Note

The export keyword is used to export an item from a JavaScript module. The default keyword defines the export as the default export, which means it can be imported without curly braces. So, a default export can be imported as import App from './App' rather than import {App} from './App'.

A method called render defines the output of the component. This method returns JSX, which, in this case, references a Layout component in our app code and a Route component from React Router.

So, we are starting to understand how React components can be composed together to form a UI.

Now, let's go through the React development experience by making a simple change:

  1. Run the app in Visual Studio by pressing F5, if it's not already running.
  2. Open the Home.js file, which can be found at ClientApp\src\components. This contains the component that renders the home page.
  3. With the app still running, in the render method, change the h1 tag in the JSX so that it renders a different string:
    render () {
      return (
        <div>
          <h1>Hello, React!</h1>
          <p>Welcome to your new single-page application,
             built with:
          </p>
          ...
        </div>
      );
    }
  4. Save the file and look at the running app:
Figure 1.17 – The home page is automatically updated in the browser

Figure 1.17 – The home page is automatically updated in the browser

The app is automatically updated with our change. The Webpack development server automatically updated the running app with the change when the file was saved. The experience of seeing our changes implemented almost immediately gives us a really productive experience when developing our React frontend.

Understanding how components access the backend web API

The final topic we'll cover in this chapter is how the React frontend consumes the backend web API. If the app isn't running, then run it by pressing F5 in Visual Studio. If we click on the Fetch data option in the top navigation bar in the app that opens in the browser, we'll see a page showing weather forecasts: 

Figure 1.18 – Weather forecast data

Figure 1.18 – Weather forecast data

If we cast our minds back to earlier in this chapter, in the Understanding controllers section, we looked at an ASP.NET Core controller that surfaced a web API that exposed the data at weatherforecast. So, this is a great place to have a quick look at how a React app can call an ASP.NET Core web API.

The component that renders this page is in FetchData.js. Let's open this file and look at the constructor class:

constructor (props) {
  super(props);
  this.state = { forecasts: [], loading: true };
}

The constructor class in a JavaScript class is a special method that automatically gets invoked when a class instance is created. So, it's a great place to initialize class-level variables.

The constructor initializes a component state, which contains the weather forecast data, and a flag to indicate whether the data is being fetched. We'll learn more about component state in Chapter 3Getting Started with React and TypeScript.

Let's have a look at the componentDidMount method:

componentDidMount() {
  this.populateWeatherData();
}

This method gets invoked by React when the component is inserted into the tree and is the perfect place to load data. This method calls a populateWeatherData method, so, let's have a look at that:

async populateWeatherData() {
  const response = await fetch('weatherforecast');
  const data = await response.json();
  this.setState({ forecasts: data, loading: false });
}

Notice the async keyword before the populateWeatherData function name. Also, notice the await keywords within the function.

Important Note

An await keyword is used to wait for an asynchronous function to complete. A function must be declared as asynchronous for us to use the await keyword within it. This can be done by placing the async keyword in front of the function name. This is very much like async and await in .NET.

We can see that a function called fetch is used within this method.

Important Note

The fetch function is a native JavaScript function for interacting with web APIs. The fetch function supersedes XMLHttpRequest and works a lot nicer with JSON-based web APIs.

The parameter that's passed into the fetch function is the path to the web API resource; that is, weatherforecast. A relative path can be used because the React app and web API are of the same origin.

Once the weather forecast data has been fetched from the web API and the response has been parsed, the data is placed in the component's state.

Hang on a minute, though – the native fetch function isn't implemented in Internet Explorer (IE). Does that mean our app won't work in IE? Well, the fetch function isn't available in IE, but CRA has set up a polyfill for this so that it works perfectly fine. 

Important Note

polyfill is a piece of code that implements a feature we expect the browser to provide natively. Polyfills allow us to develop against features that aren't supported in all browsers yet.

Now, let's turn our attention to the render method:

render () {
  let contents = this.state.loading
    ? <p><em>Loading...</em></p>
    : FetchData.renderForecastsTable(this.state.forecasts);
  return (
    <div>
      <h1 id="tabelLabel">Weather forecast</h1>
      <p>This component demonstrates fetching data from the 
        server.</p>
      {contents}
    </div>
  );
}

The code may contain concepts you aren't familiar with, so don't worry if this doesn't make sense to you at this point. I promise that it will make sense as we progress through this book!

We already know that the render method in a React component returns JSX, and we can see that JSX is returned in this render method as well. Notice the {contents} reference in the JSX, which injects the contents JavaScript variable into the markup below the p tag, at the bottom of the div tag. The contents variable is set in the first statement in the render method and is set so that Loading... is displayed while the web API request is taking place, along with the result of FetchData.renderForecastsTable when the request has finished. We'll have a quick look at this now:

static renderForecastsTable (forecasts) {
  return (
    <table className='table table-striped' aria-
      labelledby="tabelLabel">
      <thead>
        <tr>
          <th>Date</th>
          <th>Temp. (C)</th>
          <th>Temp. (F)</th>
          <th>Summary</th>
        </tr>
      </thead>
      <tbody>
        {forecasts.map(forecast =>
          <tr key={forecast.dateFormatted}>
            <td>{forecast.dateFormatted}</td>
            <td>{forecast.temperatureC}</td>
            <td>{forecast.temperatureF}</td>
            <td>{forecast.summary}</td>
          </tr>
        )}
      </tbody>
    </table>
  );
}

This function returns JSX, which contains an HTML table with the data from the forecasts data array injected into it. The map method on the forecasts array is used to iterate through the items in the array and render tr tags in the HTML table containing the data.

Important Note

The map method is a native JavaScript method that is available in an array. It takes in a function parameter that is called for each array element. The return values of the function calls then make up a new array. The map method is commonly used in JSX when iteration is needed.

Notice that we have applied a key attribute to each tr tag. What is this for? This isn't a standard attribute on an HTML table row, is it?

Important Note

The key attribute helps React detect when an element changes or is added or removed. So, it's not a standard HTML table row attribute. Where we output content in a loop, it is good practice to apply this attribute and set it to a unique value within the loop so that React can distinguish it from the other elements. Omitting keys can also lead to performance problems on large datasets as React will unnecessarily update the DOM when it doesn't need to.

Again, this is a lot to take in at this point, so don't worry if there are bits you don't fully understand. This will all become second nature to you by the end of this book.

You have been reading a chapter from
ASP.NET Core 5 and React - Second Edition
Published in: Jan 2021
Publisher: Packt
ISBN-13: 9781800206168
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime
Banner background image