React Single Page App
This tutorial will guide you through the process of deploying and hosting a single-page React application using the serverless approach.
By the end of the tutorial, you will have deployed into your AWS account:
- An AWS Lambda Function to house and deploy your React application
- The Amazon S3 Bucket that will host your static single-page React application
- An Amazon CloudFront distribution that delivers your static web content to users quickly and securely
Each step of the tutorial includes:
- A step-by-step video guide
- Transcript of the step-by-step video with code examples
This tutorial uses Professional account functionality such as Git integration and Stackery Dashboard deployment. However, each step can be completed on the Developer plan using the Stackery VS Code extension and local deployment with the Stackery CLI.
Tutorial Updates
As we continue to develop Stackery, there may be improvements made to the design of the application. You may notice some differences between current versions and versions used to record our video guides. Rest assured, the overall functionality of Stackery remains the same.
For the most up-to-date visuals and instructions, please refer to the transcribed versions of the tutorial available below each video guide.
Setup
Project Repositories
The following repository is referenced throughout this tutorial:
Required Installations
The following software is used in this tutorial:
Deploy Sample App
You can deploy the completed example into your own AWS account using these two Stackery CLI commands:
stackery create
will initialize a new repo in your GitHub account, initializing it with the contents of the referenced blueprint repository.
stackery create --stack-name 'react-single-page' \
--git-provider 'github' \
--blueprint-git-url 'https://github.com/stackery/react-single-page'
stackery deploy
will deploy the newly created stack into your AWS account.
stackery deploy --stack-name 'react-single-page' \
--env-name 'development' \
--git-ref 'master'
1. What We're Building (3 min)
A single page application is a website that does not require page reloading when in use. A web application like Gmail utilizes this functionality to improve a user's experience by pre-fetching content from a server, resulting in less wait time. After completing this tutorial, you'll have a serverless single-page application built using Stackery and React. Stackery resources will be used to configure, deploy, and host our entire application which will be built using the component-based, React library.
Another advantage to hosting your single-page application like we do in this tutorial, is having a fully-functioning app running in AWS within its free tier.
As you grow your application, be sure to monitor the cloud resources in your AWS account to ensure you're within your team, or individual, billing and cost limitations.
![The finished stack in Stackery](/docs/assets/spa-tutorial/finished-stack.png)
![The finished React app](/docs/assets/spa-tutorial/finished-spa.png)
Resources used
The following are descriptions of the Stackery resources we'll be working with:
Function : Serves two purposes. It serves as a place to house and edit our React application. It will also be where we author the Lambda function (written in Node 8) to publish the application to an S3 Bucket.
Object Store : The S3 Bucket our Function will be publishing to. It will be configured to host a static website (our React app) and be fronted by a CDN resource to speed up global distribution to our users.
CDN : Configured to serve the completed React app we're hosting in our Object Store. It will serve this content from the geographical location closest to our end-users, reducing bandwidth and loading time in the process. Also provides the SSL certificate used to access our site via HTTPS.
We'll get to configuring these resources in later parts; for now, we want to create an empty stack to start with.
Create a new stack
- Navigate to Stacks
- Select Add a Stack in the top right corner
- Select GitHub for Git Hosting Provider
- Enter
react-single-page
for Stack Name - Select Create New Repo for the Repo Source
- For Organization, select the Git account you want this repository in
- Keep the Repo Name the same as Stack Name
- Select Public for your repository's visibility (Private repositories require a paid GitHub account)
- For Stack Blueprint choose 'Blank'
- Click Add Stack to create our empty stack
Stackery initializes the following blank stack.
UP NEXT: Adding and configuring our Stackery resources.
2. Configure Function & Object Store (3.5 min)
In this section we're configuring the Function that will publish our React application, and the Object Store that will host it.
Configuring our Function
- Select Add Resource on the top right to reveal the resource panel
- Select and drag (or single-click) a Function resource to the stack editor
- Double click on the resource to open its configuration panel
- Enter
deployFrontEnd
for the Name then selectnodejs12.x
for Runtime - Enter
src/deployFrontEnd
for the Source Path to make it easier to find in our Git repository - Keep default settings for Handler and Memory
- Enter
300
for Timeout. This is to ensure this Function has enough time to package and publish our entire React application - Enable Trigger on First Deploy and then Trigger on Every Deploy when the option populates
The Trigger on Every Deploy option is only available after Trigger on First Deploy has been enabled
- We'll be configuring Permissions and Environment Variables later. Click Save
- Navigate to the left and select Commit, use the default commit message or create your own. Click Commit again
Configuring our Object Store
- Select Add Resource on the top right to reveal the resource panel
- Select and drag (or single-click) an Object Store resource to the stack editor
- Double click on the resource to open its configuration panel
- Enter
React App Host
for the Name - Select Enable Website Hosting to allow the Object Store to host a static webpage.
- Once website hosting is enabled, an additional input field will display, asking for an Index Document. Enter
index.html
then click Save
Function and Object Store service discovery
- Click and drag a Service Discovery Wire from the output port (right) of
deployFrontEnd
to the input port (left) ofReact App Host
- Double click on
deployFrontEnd
to see its effects - Scroll down to Permissions and Environment Variables and you'll notice the following additions
When we connected a Service Discovery Wire from deployFrontEnd
to React App Host
, Stackery recognized our Function requires certain IAM Policies and Environment Variables.
The S3CrudPolicy
that gets added gives deployFrontEnd
the permissions required to fully interact with our object store React App Host
. It references only our specific Object Store to go along with IAM policy best practices.
The BUCKET_NAME
and BUCKET_ARN
environment variables deployFrontEnd
are provided for us to use when writing our Lambda function. These values will dynamically populate at our function's runtime.
Additional Function configuration
- In Environment Variables add the following key-value pair. This is a file path to our Lambda specific configurations which will be explained and utilized when we write our Lambda function
- Click Save
- Navigate to the left and select Commit, use the default commit message or create your own. Click Commit
UP NEXT: Reviewing our stack's updated YAML file, and working with create-react-app.
3. Overview & Create React App (4 min)
In this section, we'll begin working with our stack's template.yaml
file locally and using create-react-app to start off our React site.
Template Editor update
For the additional configuring required in template.yaml
, you may choose to work locally, adding the necessary YAML in a code editor, or you may choose to work using the Stackery Template Editor provided within the application.
Access the stack's template editor by selecting Template under Edit Mode.
The tutorial assumes all development will be done locally after cloning the project down from the git repository.
If you choose to add configurations to your stack using the template editor provided, be sure to commit and pull changes regularly.
Working with our stack locally
- Navigate to the project repository, copy the URL, and run
git clone {REPOSITORY-URL}
in a terminal window cd
into thereact-single-page
directory and open it in a code editor. Familiarize yourself with the current folder structure
- Open the
deployFrontEnd
folder and you'll findindex.js
which is where our Lambda function will go. This folder also houses the function'spackage.json
file for required dependencies and aREADME.md
- Open
template.yaml
to find the stack's resources we orchestrated using Stackery
Under Resources
we first see our configured deployFrontEnd
Function:
Within this Function resource declaration we see the Permissions and Environment Variables granted:
The following is the resulting YAML produced by Stackery when enabling Trigger on First
and Every Deploy
. Under the hood, this Custom CloudFormation Resource triggers our deployFrontEnd
Function on every deploy.
Using Create-React-App
In the same terminal window, navigate to
src/deployFrontEnd
, the Function resource folderRun
npx create-react-app react-front-end
Once complete, head back to a code editor and familiarize yourself with the
react-front-end
folder that was just created. We'll dive deeper into our React app's structure in later sectionsHead back to the terminal window,
cd
into thereact-front-end
directory, and runnpm start
A local server will begin running and a browser window will display a default React application.
In later sections, we'll run npm start
and take advantage of Reacts's auto-reloading feature to edit our site locally, before deploying.
- Test out the auto-reloading by changing the
<p>
element inside ofApp.js
fromEdit src/App.js and save to reload.
toStackery React App
UP NEXT: Writing the Function that deploys our React application.
4. Add Deploy Function (5 min)
In this section, we'll write our deployFrontEnd
Lambda function that packages and publishes our React application to our Object Store we named React App Host
Node package installations
In a terminal window, navigate to the
deployFrontEnd
directory and run the following npm installs:npm install npm@latest
npm install cfn-custom-resource
npm install glob
npm install mime-types
Once those have been installed, the
package.json
should look like this:
- These installations created a
node_modules
folder that we don't want submitted to source control, so create a.gitignore
file in thedeployFrontEnd
folder and add node_modules to it
Deploy frontend walkthrough
- As we walk through our
deployFrontEnd
Lambda function, paste in in the following code snippets intosrc/deployFrontEnd/index.js
. The completedindex.js
can also be (found here) in the tutorials GitHub repository.
We'll go through each piece of the code that makes up our Lambda function.
We've broken down
index.js
into the following code blocks. The complete Lambda function to copy and paste is at the end of this section.
const { execFile } = require('child_process');
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
const path = require('path');
const mime = require('mime-types');
const cfnCR = require('cfn-custom-resource');
const AWS = require('aws-sdk');
var glob = require('glob');
const s3 = new AWS.S3();
First, we're using require()
to declare the modules we need and storing them in variables we'll be referencing throughout our function. These modules will help with organizing and packaging our React application and we'll see how they're used in our handler function up next.
exports.handler = async message => {
console.log(message);
try {
const tmpDir = `/tmp/front-end${process.pid}`;
const npm = 'npm';
await spawnPromise('rm', ['-rf', tmpDir]);
await spawnPromise('cp', ['-R', 'react-front-end/', tmpDir]);
await spawnPromise(
npm,
['--production',
'--no-progress',
'--loglevel=error',
'--cache', path.join('/tmp', 'npm'),
'--userconfig', path.join('/tmp', 'npmrc'),
'install'
],
{cwd: tmpDir}
);
await spawnPromise(
npm,
['--production',
'--no-progress',
'--loglevel=error',
'--cache', path.join('/tmp', 'npm'),
'--userconfig', path.join('/tmp', 'npmrc'),
'run', 'build'
],
{cwd: tmpDir}
);
const builtPaths = glob.sync(`${tmpDir}/build/**/*`);
console.log(builtPaths);
builtPaths.forEach(async (path) => {
if (!fs.lstatSync(path).isFile()) {
return;
}
const mimeType = mime.lookup(path) || 'application/octet-stream';
console.log(mimeType);
const fileHandle = await readFile(path);
const key = path.replace(`${tmpDir}/build/`, '');
const params = {
ACL: 'public-read',
ContentType: mimeType,
Body: fileHandle,
Bucket: process.env.BUCKET_NAME,
Key: key
};
console.log(params);
const s3Response = await s3.putObject(params).promise();
console.log(s3Response);
});
await cfnCR.sendSuccess('deployFrontEnd', {}, message);
} catch (error) {
console.log(error);
await cfnCR.sendFailure(error.message, message);
}
};
Our handler function, the function's entry point, is next. Below the console.log(message);
we see the beginning of our try/catch/finally statement. The try
statement consists of our efforts to build and package our React application that originates from within this Function resource.
Each spawnPromise()
call is a shell command we're running within Lambda. In the first two, we're removing our Lambda function's tmpDir
folder and replacing it with the contents of our react-front-end
folder stored alongside this function. The next two run the npm install
and npm build
commands with additional production arguments to build out our React application. We declare our spawnFunction()
at the bottom of index.js
. Our deployFrontEnd
Lambda function is essentially clearing out any existing React application and replacing it with our edited version on each deploy.
Next we're using glob.sync()
to include all of the contents of the resulting /build
folder, regardless of file type, and store them into the variable builtPaths
. Then we're iterating through each file in buildPaths
and gathering what we need to get them uploaded to S3. For each file, we're using mime.lookup()
to determine their content type and readFile()
to read and collect the content. We're removing the ${tmpDir}/build/
on each file to use the file name as a key when we upload to S3.
We construct each object by passing in the variables to satisfy its ContentType
, Body
, and Key
. Along with those values, we're including a public-read
value for each object's ACL in order for them to be publicly accessible and viewed online. The S3 BUCKET_NAME
environment variable gathered using Stackery comes into play to define which bucket we're placing our objects into. Next up we call s3.putObject(params)
for each object to upload our React application to our web-hosting bucket. Then we wrap up our try
block by calling await cfnCR.sendSuccess('deployFrontEnd', {}, message)
to signal to our custom resource that our Function has been triggered and was successful in completing it's task.
If at any point in our try
, there is an error, we'll log and send a specific failure response in the catch
statement. When we configured our Function resource in Stackery, we instructed CloudFormation to trigger deployFrontEnd
on every deploy by enabling the two Trigger on First/Every Deploy
properties. Under the hood, Stackery added a CloudFormation Custom Resource responsible for triggering our function. Again, since it's in charge of triggering our function, we need the cfn-resource
module's sendFailure()
and sendSuccess()
calls to report a status back to our custom resource.
function spawnPromise (command, args, options) {
console.log(`Running \`${command} '${args.join("' '")}'\`...`);
options = options || {};
if (!options.env) {
options.env = {};
}
Object.assign(options.env, process.env);
return new Promise((resolve, reject) => {
execFile(command, args, options, (err, stdout, stderr) => {
console.log('STDOUT:');
console.log(stdout);
console.log('STDERR:');
console.log(stderr);
if (err) {
err.stdout = stdout;
err.stderr = stderr;
reject(err);
} else {
resolve({stdout: stdout, stderr: stderr});
}
});
});
}
The last part of index.js
is our spawnPromise
function declaration. The first console.log
will display in our CloudWatch logs for this function and indicate which command is being executed at the time. Next we're gathering and assigning any options that may have been passed through to spawnPromise()
. Finally, we construct and return a Promise()
which will utilize execFile
from the child_process
module we declared at the top. execFile()
will take in the command we want to run, any arguments to attach to it, options, and some callbacks. The promise will either carry an error or a successful command output.
If you've chosen to use different names for your Function resource name and/or React application name,
Replace
react-front-end
with the name of your react application folder
Replace
deployFrontEnd
with the name of your Function resource folder
Here's the complete Lambda function to paste into your index.js
const { execFile } = require('child_process');
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
const path = require('path');
const mime = require('mime-types');
const cfnCR = require('cfn-custom-resource');
const AWS = require('aws-sdk');
var glob = require('glob');
const s3 = new AWS.S3();
exports.handler = async message => {
console.log(message);
try {
const tmpDir = `/tmp/front-end${process.pid}`;
const npm = 'npm';
await spawnPromise('rm', ['-rf', tmpDir]);
await spawnPromise('cp', ['-R', 'react-front-end/', tmpDir]);
await spawnPromise(
npm,
['--production',
'--no-progress',
'--loglevel=error',
'--cache', path.join('/tmp', 'npm'),
'--userconfig', path.join('/tmp', 'npmrc'),
'install'
],
{ cwd: tmpDir }
);
await spawnPromise(
npm,
['--production',
'--no-progress',
'--loglevel=error',
'--cache', path.join('/tmp', 'npm'),
'--userconfig', path.join('/tmp', 'npmrc'),
'run', 'build'
],
{ cwd: tmpDir }
);
const builtPaths = glob.sync(`${tmpDir}/build/**/*`);
console.log(builtPaths);
builtPaths.forEach(async (path) => {
if (!fs.lstatSync(path).isFile()) {
return;
}
const mimeType = mime.lookup(path) || 'application/octet-stream';
console.log(mimeType);
const fileHandle = await readFile(path);
const key = path.replace(`${tmpDir}/build/`, '');
const params = {
ACL: 'public-read',
ContentType: mimeType,
Body: fileHandle,
Bucket: process.env.BUCKET_NAME,
Key: key
};
console.log(params);
const s3Response = await s3.putObject(params).promise();
console.log(s3Response);
});
await cfnCR.sendSuccess('deployFrontEnd', {}, message);
} catch (error) {
console.log(error);
await cfnCR.sendFailure(error.message, message);
}
};
function spawnPromise(command, args, options) {
console.log(`Running \`${command} '${args.join("' '")}'\`...`);
options = options || {};
if (!options.env) {
options.env = {};
}
Object.assign(options.env, process.env);
return new Promise((resolve, reject) => {
execFile(command, args, options, (err, stdout, stderr) => {
console.log('STDOUT:');
console.log(stdout);
console.log('STDERR:');
console.log(stderr);
if (err) {
err.stdout = stdout;
err.stderr = stderr;
reject(err);
} else {
resolve({ stdout: stdout, stderr: stderr });
}
});
});
}
UP NEXT: Our first stack deploy and viewing our React application hosted from an S3 Bucket.
5. Bucket Hosted Single Page App (2.5 min)
In this section, we're deploying our stack for the first time and getting to see our initial React application hosted from an S3 bucket.
Prepare and deploy the stack
- In Stackery, we notice our left navigation panel has a remote change alert. Click refresh to pull the changes we made using our code editor in the previous section.
A remote change alert will display whenever there are changes made and pushed to the hosting git repository outside of the current Stackery browser tab. This alert provides you the option to create a new branch and keep the stack in the current session, or to update the stack in the current session with a refresh. If this alert does not show for you, simply refresh your browser to display any changes.
- Once the stack is refreshed, select Prepare Deployment
A deployment preparation may take up to 5 minutes.
- Once the deployment has finished preparing, select Deploy. A new browser window will pop up showing the AWS console
- The AWS console will display the stack change set and any updates to our stack under the Changes tab
- We're satisfied with these proposed changes so we'll click Execute to officially deploy our stack
This current stack deployment may take up to 5-10 minutes. Deployment will continue for this stack even if you close the browser window.
- Back in Stackery, we're presented with a notification indicating our stack has finished deployment. You'll also notice a red indicator on the navigation's View tab
- On the Deployments page, double-click on
React App Host
to reveal the deployed resource properties and select the S3 Bucket'sARN
- The new AWS console displays our S3 Bucket with the files which make up our React website
- Navigate to the Properties tab in the current console and then click on Static website Hosting. It will open up and reveal an S3 HTTP endpoint to our React website
- A new browser window opens with our React app successfully being hosted from our
React App Host
Object Store
UP NEXT: Now that our React site is being hosted from our S3 Bucket, we're going to speed up the delivery of our content to our users by configuring a CDN.
6. Configure CDN (3 min)
In this section, we're adding a CDN to our current stack to speed up delivery to our end-users.
Configuring our CDN
- Select Add Resource on the top right to reveal the resource panel
- Select and drag (or single-click) a CDN resource to the stack editor
- Double-click on the resource to open its configuration panel
- Enter
Our CDN
for the Name. Click Save - Click and drag an Integration Wire from the output port (right) of
Our CDN
to the input port (left) ofReact App Host
- Navigate to the left and select Commit, use the default commit message or create your own. Click Commit
If you plan to use a custom domain for your React website, enabling the
Use Custom Domain
property will prompt you for theDomain
you want to use, as well as theValidation Domain
used to validate it.
Additional CDN Configuration
- From a terminal window, and inside the project directory, run
git pull
to adopt the changes we made with Stackery - There were additions made to our
template.yaml
file that we'll briefly go over and include some changes of our own
The last resource in template.yaml
(before the stack's parameters), is the resulting bucket policy supplied by Stackery when utilizing an integration wire (solid wire).
Under our React App Host
Object Store configurations, we see the CDN that we added Our CDN
. Towards the bottom of this resource we see an Origins statement. It declares the CDN's source and is another result of connecting Our CDN
with React App Host
using an integration wire.
- Include the following line of YAML into the
Our CDN
resource. Place it above, and on the same indentation level as the Enabled property:
DefaultRootObject: index.html
Our CDN
now has a DefaultRootObject property, which defines the object from our S3 Bucket to return and serve. In this case theindex.html
file fromReact App Host
, our S3 Bucket.
This is our final template.yaml
edit and it should look like the following:
Prepare and deploy stack
- Head back to Stackery stack editor, navigate to the left towards the remote changes alert, click refresh
- Select Prepare Deployment
- Once the deployment has been prepared, select Deploy
- In the browser window that pops up you'll see all stack changes towards the bottom left of the AWS console
- Once the changes have been reviewed, select Execute in the top right corner
- Confirm this change set execution by selecting Execute in the modal to deploy your stack
The AWS Console navigates to an update stack detail page and you'll see an UPDATE_IN_PROGRESS status in orange. Refresh the page if it does not show initially.
This stack deployment that includes the addition of our CDN may take up to 15 minutes. Deployment will continue for this stack even if you close this browser window.
React app on CloudFront
- Once the stack deployment is complete, navigate back to Stackery and select the View tab at the top left. It should show a red deployment change indicator:
- On the Deployments page, double-click on
Our CDN
to reveal the deployed resource properties - Copy the Distribution DNS Name to your clipboard
- In a new browser tab, paste and go to the cloudfront.net URL
The browser will load our React application (which can be verified by the Stackery React App title) and is now serving our content from the AWS Edge location closest to us!
UP NEXT: Now that our serverless infrastructure and workflow has been determined with Stackery, the rest of this tutorial consists of a two-part implementation of React and React Router to build out our website's front end.
7. Working with React I (3.5 min)
In this section, we're establishing the foundation of our React single page application for you to continue developing after this tutorial. Our serverless infrastructure has been configured and we've established a workflow.
Configure your React app
Install React Router
- In a terminal window,
cd
intoreact-front-end
- Run
npm i react-router-dom --save
to download React Router - From the same
react-front-end
directory, runnpm start
to start the local server to begin editing
We'll briefly go over our React application's current component tree, starting from App.js
and ending with the index.html
file that we defined as our S3 Bucket's index document.
React App Overview
The following is our App.js
file located at react-front-end/src/App.js
. We've edited the <p>
element in a previous section to display Stackery React App
.
App.js
is our main component and will hold the other components of our app. This is where we'll orchestrate and route the content that makes up our single-page application.
In index.js
, located in the same /src
folder, we want to highlight line 7 that reads the following:
ReactDOM.render(<App />, document.getElementById('root'));
This line is telling our application to render App.js
in the DOM where the id is 'root'.
index.html
is where our application gets rendered. More specifically, the <div>
with the id of 'root'. We'll also update the <title>
element with My Serverless App
and see our changes take effect immediately.
Now that we have an idea of where to orchestrate our application's components, and where our components will be rendered, let's organize our project.
Components and Images
- In a code editor or terminal window, add a
components
andimages
directory into thesrc
folder ofreact-front-end
- Add a new file to the
src/components
folder, name itHome.js
and paste the following code in:
import React, { Component } from 'react';
import '../App.css';
class Home extends Component {
render() {
return (
<div className="home">
<h2>NICE JOB</h2>
<p>Your Serverless Single Page Application</p>
</div>
);
}
}
export default Home;
At the top of Home.js
we're importing the React
and Component
classes to use in our code, as well as importing the styles we'll define later in App.css
.
Next we're constructing our Home
component which returns JSX, and will render an <h2>
and <p>
element.
Finally at the bottom, we're exporting our Home
component and will import it into other components whenever we'd like to render it on the DOM.
This is the format of the other two components our React app will render, so add a Content.js
and an About.js
file to src/components
and paste in the following code blocks:
src/components/Content.js
import React, { Component } from "react";
import '../App.css';
class Post extends Component {
render() {
return (
<div className="post">
<h2>YOUR CONTENT</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
</div>
);
}
}
export default Post;
src/components/About.js
import React, { Component } from "react";
import '../App.css';
import stackeryLogo from '../images/stackery-logo.png';
import reactLogo from '../images/react-logo.png';
class About extends Component {
render() {
return (
<div className="about">
<a target="_blank" href="https://www.stackery.io">
<img src={stackeryLogo} className="stackery-logo" alt="stackery-logo" />
</a>
<p className="plus">+</p>
<a target="_blank" href="https://reactjs.org/">
<img src={reactLogo} className="react-logo" alt="react-logo" />
</a>
</div>
);
}
}
export default About;
Upload any images you'd like to your
src/images
folder forAbout.js
, or delete the image imports and related JSX all together. Make sure to updateAbout.js
to import and render any images you upload to yoursrc/images
folder.
Commit Folder Updates
- In a terminal window, run
git add .
andgit commit
to add the changes we've made toreact-front-end
.
UP NEXT: We'll tie in our new components and add routing to our single-page application with react-router.
8. Working With React II (3.5 min)
In this section, we wrap up our work with React and initiate our final deploy.
App.js + React Router
- Delete the contents of
App.js
and paste in the following code block. We'll go over the new additions in more detail
import React, { Component } from 'react';
import { Route, NavLink, HashRouter } from 'react-router-dom';
import Home from './components/Home';
import Content from './components/Content';
import About from './components/About';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<HashRouter>
<div className="container">
<ul className="nav">
<li><NavLink exact to="/">Home</NavLink></li>
<li><NavLink to="/content">Content</NavLink></li>
<li><NavLink to="/about">About</NavLink></li>
</ul>
<div className="pages">
<Route exact path="/" component={Home}/>
<Route path="/content" component={Content}/>
<Route path="/about" component={About}/>
</div>
</div>
</HashRouter>
</div>
);
}
}
export default App;
The import statements at the top now include the components we created in the previous section, as well as the Route, NavLink, and HashRouter components from react-router-dom. We see the react-router-dom components in action within <div className="App">
below.
We're using the <HashRouter />
component to house our <NavLink />
and <Route />
components.
NavLink components represent the links that will render in our HTML.
Route components specify the path each link represents, and which component to render.
As soon as we pasted in the updated App.js
, the browser refreshed with our changes but didn't have any styling. Let's add to our App.css
file to fix that. Paste in the following and after a refresh your browser will reflect some basic styling.
body {
padding: 20px;
margin: 0;
background-color: #fff;
}
h1, h2, p, ul, li {
font-family: "Open Sans",sans-serif;
}
ul {
text-align: center;
}
ul.nav li {
display: inline;
list-style-type: none;
margin: 0;
}
ul.nav {
padding: 0;
}
ul.nav li a {
color: black;
font-weight: bold;
text-decoration: none;
padding: 20px;
display: inline-block;
}
ul.nav li a:hover {
color:#79ccb5;
}
.pages {
text-align: center;
background-color: #fff;
padding: 20px;
}
.pages h2 {
padding: 0;
margin: 0;
}
.home, .stackery, .about{
width: 800px;
margin: 0 auto;
margin-top: 20px;
padding: 20px;
color: #343a40;
}
.active {
border-bottom: 5px solid #79ccb5;
}
.stackery-logo, .react-logo{
width: 250px;
}
.plus{
font-size: 3rem;
}
Another look at our browser and we'll see our completed React single-page application. Clicking any of the links at the top will render its component below, all without having to reload the browser. Also take note of the URL paths as a result of selecting each link - we're seeing our react router components at work!
Commit React App Changes
- In a terminal window,
cd
back to thereact-single-page
project directory - Run
git add .
followed bygit commit - "final commit message"
- Run
git push
to push all the React app changes we've made to your git repository
Now that we're satisfied with the functionality and look of our React site, let's head back to the Stackery Dashboard one last time for the last deploy.
Final deploy
- In Stackery, navigate to the Edit tab in our side panel (refresh if necessary), and select Prepare Deployment
- Once our deployment is prepared, select Deploy at the bottom left of our side panel
- In the AWS console that pops up, there's another change set request presented for review, select Execute in the top right corner to deploy our stack
- Once the deploy is complete, we'll head back into Stackery to our stack's View tab
- Double-click on
Our CDN
to open the deployed resource properties, copy the Distribution DNS Name and paste it in a new tab
The URL is the default CloudFront domain, but you'll notice the #/{Path Name}
that gets added to the end of it and updates whenever you click on a navigation link. Our site is now live, gets delivered quickly to our users with our CDN, and dynamically renders content with the help of React Router.
There you have it! Your completed, serverless single-page application built using Stackery and React!
Troubleshooting
Common errors you may come across when following along with this tutorial.
Create Custom Resource takes more than 15 minutes
If a Function resource in your stack has Trigger on First Deploy and/or Trigger on Every Deploy, Stackery will create a custom resource responsible for triggering your Lambda function. While working through this tutorial, if a stack deployment is taking longer than 10-15 minutes and maintains the CREATE_IN_PROGRESS event for Custom::FunctionDeployTrigger
, it may be due to a Lambda function timeout/failure.
You can confirm this by accessing the CloudWatch logs for your Lambda function.
CloudFormation will continue this deployment waiting for a success response from the custom resource indicating that the Lambda function has been triggered. If a success response is not received after 60 minutes, it will fail to create the resource and begin a DELETE_IN_PROGRESS event that may take an additional 60 minutes to complete. This resulting delete event for the custom resource could also fail, and CloudFormation will retry this deletion up to two more times.
Use the following to "escape" this process by running a curl response indicating a SUCCESS
while the custom resource is in the CREATE_IN_PROGRESS event.
In the AWS Console, navigate to your Lambda function's CloudWatch Logs
Access the most recent Log Stream
Locate the log with a message that consists of
START
The log directly below it holds the values we need for our curl request (message will have RequestType, ServiceToken, etc)
Copy and save the following values from this log
ResponseURL
StackId
RequestId
LogicalResourceId
Use the following to construct the curl command by replacing the placeholders with the values from your CloudWatch log
curl -X PUT '{RESPONSE_URL}' -H 'Content-Type:' -d '{"Status":"SUCCESS","Reason":"Manual Success","PhysicalResourceId":"resource","RequestId":"{REQUEST_ID}","LogicalResourceId":"{LOGICAL_RESOURCE_ID}","StackId":"{STACK_ID}"}'
- Run your curl command in a terminal window
- Head back to your stack's CloudFormation console to verify the success message has been received and a CREATE_COMPLETE event displays for
Custom::FunctionDeployTrigger