Single Page Applications
Overview: Single Page Applications
- In a multi-page website, you have a different HTML page for each view, such as in your Bootstrap Project.
- While each page had parts in common (header, footer, navbar, etc), every new page you went to resulted in a new server request, all the code being rendered again in the browser.
- This can be expensive process.
Single Page Application (SPA)
- One page that locally downloads all the front-end application code for the website (HTML, CSS, JS)
- Examples: Gmail, Google Maps, Facebook
- When switching views, the application intelligently handles the front-end rendering, making minimum request to the server (for database data, for example) and re-rendering only what’s changed.
Pros of SPA
- Typically much faster due to fewer server request
- Streamlined development: Easier to divide back end and front end tasks
- Easily reuse back end code for mobile app development
- Easy to debug in browser
CONS of SPA
- Search engines can have trouble indexing SPAs (though they are improving), so SPA’s are typically best for websites behind a login that would not be indexed anyway.
- Dependent on JavaScript being enabled in browser
- Initial download of application code can be slow
- Must be very careful of memory leaks
Navigation and Routing in SPA
- Does not use traditional browser navigation of going to new HTML page for each view.
- Concerns such as clicking on links to render different views, using Back/Forward button for browser history, etc. Note: This is what React Router Provides
Adding to the Single Page App
Added a new Contact component and learned how to route to it using react-router.
ContactComponent.js
import React from ‘react’;
function Contact(props) {
return (
<div className=”container”>
<div className=”row”>
<div className=”col”>
<h2>Contact Us</h2>
<hr />
</div>
</div>
<div className=”row row-content align-items-center”>
<div className=”col-sm-4″>
<h5>Our Address</h5>
<address>
1 Nucamp Way<br />
Seattle, WA 98001<br />
U.S.A.
</address>
</div>
<div className=”col”>
<a role=”button” className=”btn btn-link” href=”tel:+12065551234″><i className=”fa fa-phone” /> 1-206-555-1234</a><br />
<a role=”button” className=”btn btn-link” href=”mailto:fakeemail@fakeemail.co”><i className=”fa fa-envelope-o” /> campsites@nucamp.co</a>
</div>
</div>
</div>
);
}
export default Contact;
MainComponent.js
import React, { Component } from ‘react’;
import Directory from ‘./DirectoryComponent’;
import CampsiteInfo from ‘./CampsiteInfoComponent’;
import Header from ‘./HeaderComponent’;
import Footer from ‘./FooterComponent’;
import Home from ‘./HomeComponent’;
import Contact from ‘./ContactComponent’;
import { Switch, Route, Redirect } from ‘react-router-dom’;
import { CAMPSITES } from ‘../shared/campsites’;
import { COMMENTS } from ‘../shared/comments’;
import { PARTNERS } from ‘../shared/partners’;
import { PROMOTIONS } from ‘../shared/promotions’;
class Main extends Component {
constructor(props) {
super(props);
this.state = {
campsites: CAMPSITES,
comments: COMMENTS,
partners: PARTNERS,
promotions: PROMOTIONS,
};
}
render() {
const HomePage = () => {
return (
<Home
campsite={this.state.campsites.filter(campsite => campsite.featured)[0]}
promotion={this.state.promotions.filter(promotion => promotion.featured)[0]}
partner={this.state.partners.filter(partner => partner.featured)[0]}
/>
);
};
return (
<div>
<Header />
<Switch>
<Route path=’/home’ component={HomePage} />
<Route exact path=’/directory’ render={() => <Directory campsites={this.state.campsites} />} />
<Route exact path=’/contactus’ component={Contact} />
<Redirect to=’/home’ />
</Switch>
<Footer />
</div>
);
}
}
export default Main;
HomeComponent.js
import React from ‘react’;
import { Card, CardImg, CardText, CardBody, CardTitle } from ‘reactstrap’;
function RenderCard({item}) {
return (
<Card>
<CardImg src={item.image} alt={item.name} />
<CardBody>
<CardTitle>{item.name}</CardTitle>
<CardText>{item.description}</CardText>
</CardBody>
</Card>
);
}
function Home(props) {
return (
<div className=”container”>
<div className=”row”>
<div className=”col-md m-1″>
<RenderCard item={props.campsite} />
</div>
<div className=”col-md m-1″>
<RenderCard item={props.promotion} />
</div>
<div className=”col-md m-1″>
<RenderCard item={props.partner} />
</div>
</div>
</div>
);
}
export default Home;
React Router Parameters
Right now in our web app, clicking on a campsite in the directory is no longer showing the information for that campsite.
Later we will learn to use parameters and React Router to access that information. First you set it up so that when you click on a campsite, your React code will automatically grab the ID of that campsite and add it to the path as you can see up here in the address bar.
So these urls are not hard-coded, they are generated automatically based on the campsite data. Then the route component will use this ID part of the URL, which here is three, then it will send the campsite object with that ID to the campsite info component and render the information for that campsite. To let the route component know that this part after directory is going to be a dynamic route parameter, we will use this attribute path=’/directory/:campsiteID‘ .
This colon here will cause the route component to grab whatever string occurs after directory in the path then store that string inside a route parameter called campsiteID . That route parameter is the property of the route component state. Specifically it’s stored inside a state object called match as a property named params.
We can look inside the react dev tools components tool and if we select this route component. We can see that it has a state with a match object and a params property which is also an object. Inside that is the campsite id with a value of 3. This stored campsite id can be used to pull up the campsite object with that ID and that object is then passed into the campsite info component.
React Router Parameters
You learned how to use dynamically generated URL links with React Router parameters to generate a specific view with the CampsiteInfo component.
DirectoryComponent.js
import React from ‘react’;
import { Card, CardImg, CardImgOverlay, CardTitle } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
function RenderDirectoryItem({campsite}) {
return (
<Card>
<Link to={`/directory/${campsite.id}`}>
<CardImg width=”100%” src={campsite.image} alt={campsite.name} />
<CardImgOverlay>
<CardTitle>{campsite.name}</CardTitle>
</CardImgOverlay>
</Link>
</Card>
);
}
function Directory(props) {
const directory = props.campsites.map(campsite => {
return (
<div key={campsite.id} className=”col-md-5 m-1″>
<RenderDirectoryItem campsite={campsite} />
</div>
);
});
return (
<div className=”container”>
<div className=”row”>
{directory}
</div>
</div>
);
}
export default Directory;
MainComponent.js
import React, { Component } from ‘react’;
import Directory from ‘./DirectoryComponent’;
import CampsiteInfo from ‘./CampsiteInfoComponent’;
import Header from ‘./HeaderComponent’;
import Footer from ‘./FooterComponent’;
import Home from ‘./HomeComponent’;
import Contact from ‘./ContactComponent’;
import { Switch, Route, Redirect } from ‘react-router-dom’;
import { CAMPSITES } from ‘../shared/campsites’;
import { COMMENTS } from ‘../shared/comments’;
import { PARTNERS } from ‘../shared/partners’;
import { PROMOTIONS } from ‘../shared/promotions’;
class Main extends Component {
constructor(props) {
super(props);
this.state = {
campsites: CAMPSITES,
comments: COMMENTS,
partners: PARTNERS,
promotions: PROMOTIONS,
};
}
render() {
const HomePage = () => {
return (
<Home
campsite={this.state.campsites.filter(campsite => campsite.featured)[0]}
promotion={this.state.promotions.filter(promotion => promotion.featured)[0]}
partner={this.state.partners.filter(partner => partner.featured)[0]}
/>
);
};
const CampsiteWithId = ({match}) => {
return (
<CampsiteInfo
campsite={this.state.campsites.filter(campsite => campsite.id === +match.params.campsiteId)[0]}
comments={this.state.comments.filter(comment => comment.campsiteId === +match.params.campsiteId)}
/>
);
};
return (
<div>
<Header />
<Switch>
<Route path=’/home’ component={HomePage} />
<Route exact path=’/directory’ render={() => <Directory campsites={this.state.campsites} />} />
<Route path=’/directory/:campsiteId’ component={CampsiteWithId} />
<Route exact path=’/contactus’ component={Contact} />
<Redirect to=’/home’ />
</Switch>
<Footer />
</div>
);
}
}
export default Main;
CampsiteInfoComponent.js
import React from ‘react’;
import { Card, CardImg, CardText, CardBody, CardTitle } from ‘reactstrap’;
function RenderCampsite({campsite}) {
return (
<div className=”col-md-5 m-1″>
<Card>
<CardImg top src={campsite.image} alt={campsite.name} />
<CardBody>
<CardTitle>{campsite.name}</CardTitle>
<CardText>{campsite.description}</CardText>
</CardBody>
</Card>
</div>
);
}
function RenderComments({comments}) {
if (comments) {
return (
<div className=”col-md-5 m-1″>
<h4>Comments</h4>
{comments.map((comment) => {
return (
<p key={comment.id}>
{comment.text} <br />
{comment.author}{” “}
{new Intl.DateTimeFormat(“en-US”, {
year: “numeric”,
month: “short”,
day: “2-digit”,
}).format(new Date(Date.parse(comment.date)))}
</p>
);
})}
</div>
);
}
return <div />;
}
function CampsiteInfo(props) {
if (props.campsite) {
return (
<div className=”container”>
<div className=”row”>
<RenderCampsite campsite={props.campsite} />
<RenderComments comments={props.comments} />
</div>
</div>
);
}
return <div />;
}
export default CampsiteInfo;
ADD BREADCRUMBS
You added Breadcrumbs to your app with the help of the Reactstrap library.
DirectoryComponent.js
import React from ‘react’;
import { Card, CardImg, CardImgOverlay, CardTitle, Breadcrumb, BreadcrumbItem } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
function RenderDirectoryItem({campsite}) {
return (
<Card>
<Link to={`/directory/${campsite.id}`}>
<CardImg width=”100%” src={campsite.image} alt={campsite.name} />
<CardImgOverlay>
<CardTitle>{campsite.name}</CardTitle>
</CardImgOverlay>
</Link>
</Card>
);
}
function Directory(props) {
const directory = props.campsites.map(campsite => {
return (
<div key={campsite.id} className=”col-md-5 m-1″>
<RenderDirectoryItem campsite={campsite} />
</div>
);
});
return (
<div className=”container”>
<div className=”row”>
<div className=”col”>
<Breadcrumb>
<BreadcrumbItem><Link to=”/home”>Home</Link></BreadcrumbItem>
<BreadcrumbItem active>Directory</BreadcrumbItem>
</Breadcrumb>
<h2>Directory</h2>
<hr />
</div>
</div>
<div className=”row”>
{directory}
</div>
</div>
);
}
export default Directory;
ContactComponent.js
import React from ‘react’;
import { Breadcrumb, BreadcrumbItem } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
function Contact(props) {
return (
<div className=”container”>
<div className=”row”>
<div className=”col”>
<Breadcrumb>
<BreadcrumbItem><Link to=”/home”>Home</Link></BreadcrumbItem>
<BreadcrumbItem active>Contact Us</BreadcrumbItem>
</Breadcrumb>
<h2>Contact Us</h2>
<hr />
</div>
</div>
<div className=”row row-content align-items-center”>
<div className=”col-sm-4″>
<h5>Our Address</h5>
<address>
1 Nucamp Way<br />
Seattle, WA 98001<br />
U.S.A.
</address>
</div>
<div className=”col”>
<a role=”button” className=”btn btn-link” href=”tel:+12065551234″><i className=”fa fa-phone” /> 1-206-555-1234</a><br />
<a role=”button” className=”btn btn-link” href=”mailto:fakeemail@fakeemail.co”><i className=”fa fa-envelope-o” /> campsites@nucamp.co</a>
</div>
</div>
</div>
);
}
export default Contact;
CampsiteInfoComponent.js
import React from ‘react’;
import { Card, CardImg, CardText, CardBody, Breadcrumb, BreadcrumbItem } from ‘reactstrap’;
import { Link } from ‘react-router-dom’;
function RenderCampsite({campsite}) {
return (
<div className=”col-md-5 m-1″>
<Card>
<CardImg top src={campsite.image} alt={campsite.name} />
<CardBody>
<CardText>{campsite.description}</CardText>
</CardBody>
</Card>
</div>
);
}
function RenderComments({comments}) {
if (comments) {
return (
<div className=”col-md-5 m-1″>
<h4>Comments</h4>
{comments.map((comment) => {
return (
<p key={comment.id}>
{comment.text} <br />
{comment.author}{” “}
{new Intl.DateTimeFormat(“en-US”, {
year: “numeric”,
month: “short”,
day: “2-digit”,
}).format(new Date(Date.parse(comment.date)))}
</p>
);
})}
</div>
);
}
return <div />;
}
function CampsiteInfo(props) {
if (props.campsite) {
return (
<div className=”container”>
<div className=”row”>
<div className=”col”>
<Breadcrumb>
<BreadcrumbItem><Link to=”/directory”>Directory</Link></BreadcrumbItem>
<BreadcrumbItem active>{props.campsite.name}</BreadcrumbItem>
</Breadcrumb>
<h2>{props.campsite.name}</h2>
<hr />
</div>
</div>
<div className=”row”>
<RenderCampsite campsite={props.campsite} />
<RenderComments comments={props.comments} />
</div>
</div>
);
}
return <div />;
}
export default CampsiteInfo;
Codepen: React Router Params
Additional Resources: