The goal of this workshop is to get you acquainted with ECMAScript 2015, otherwise known as ES6 (quick ref), and ReactJS. We'll build a client-side React app together, and then enhance it with universal rendering in node.js (more on that later!)
By the end of this session you will:
- Have a working React web application
- Understand the most useful and interesting concepts of ES6
- Be aware of ES6 compatibility issues and how to overcome them using Babel
- Gain an understanding of universal rendering and how to implement it easily
For this tutorial, we'll be creating a small video player app.
We're going to use the following technologies: React, Babel, react-router, webpack, node.js, express, and npm. Don't worry if you're not familiar with these, I'll introduce them as we use them.
ES6 is the latest version of the ECMAScript standard, and it supersedes ES5, which was standardized in 2009. A lot has changed since 2009: Internet Explorer 8 was the most popular browser, and IE represented the lion's share of usage at approximately 70% (source: Wikipedia). JavaScript's latest features are very interesting and go a long way to improve some of the issues with the language's "bad parts", and introduces some awesome new features.
Unfortunately or fortunately, depending on your perspective, ES6 has introduced breaking changes. While billions of devices can read and interpret JavaScript, from web browsers to servers to Raspberry Pi to "smart home" devices, it is important to be mindful that many of them don't currently or will never support ES6 syntax. This shouldn't stop us from writing the latest code, though. Browser support is rapidly improving, and there is a workaround, transpilation (we'll go over this later).
These steps are written with MacOS in mind. If you have a different operating system, your mileage may vary.
-
(If you don't have
git
installed, follow the instructions here) -
Clone this repository. In your terminal:
git clone https://github.com/tedwards947/es6-react-workshop.git
-
If you're following along, do:
git checkout first
This will give you a clean place to start.
-
Install the dependencies, babel, express, react, react-router, classnames, webpack, babel-loader, and http-server.
From the directory you checked the project out to, in your terminal, do:
npm install
This tells npm to install the dependencies found in the project's
package.json
file.
- Copy
node_modules.zip
from my flash drive to the directory you just cloned from GitHub and extract it.
You need to host the images and videos used in this workshop locally.
(If you're doing these steps outside the context of
a physical workshop, you'll need to come up with your own assets (try archive.org) and adjust the data.js
file accordingly.)
- Copy
assets.zip
from my flash drive to the directory you just cloned from GitHub and extract it.
We're going to use the npm package http-server to act as a simple CDN (content delivery network) for us.
-
In your terminal, from within the
es6-react-workshop
directory, do:./node_modules/http-server/bin/http-server ./assets -p 8082
You should see output very similar to this:
Starting up http-server, serving ./assets
Available on:
http://localhost:8082
Hit CTRL-C to stop the server
Important: Leave this running. Open a new terminal tab/window to do subsequent steps.
Look at src/data.js
. Below is an example entry:
{
"title": "Kung Fu Hustle",
"thumbnailUrl": "http://localhost:8082/thumbs/KungFuHustle.jpg",
"heroUrl": "http://localhost:8082/heroes/KungFuHustle.png",
"video": {
"url": "http://localhost:8082/videos/KungFuHustleTrailer.mp4"
},
"id": 0
}
Each entry contains information about the video:
title
: the title of the videothumbnailUrl
: the image URL for the small-sized static "poster" imageheroUrl
: the image URL for the large-sized static "poster" image, used when the video is queued but not playingvideo.url
: the URL of the actual video fileid
: the ID of the video. For this example, they are numeric, but they needn't be.
Notice how the URLs contain localhost:8082
. This will point to the http-server instance you set running earlier on your machine.
Take a look at src/static/index.html
. Most of it is pretty standard HTML boilerplate, so I won't go into much detail. Notice this line, however:
<div id="main"></div>
This is where we'll tell React to inject itself in our page.
Speaking of React, let's get to it!
React is a declarative, component-based way to write user interfaces. Rather than requiring the user (that's you!) to learn a bunch of special jargon and syntax, it uses syntax that most JavaScript developers are likely to be familiar with.
Let's take a look at a quick example to help you visualize React and how it compares to AngularJS and "vanilla" (plain) JavaScript.
How might we write some code that accepts an array of data and writes the items to
<li>
elements in an ordered list (<ol>
)?
For the following examples, the data we'll be using is:
const names = [
'Theresa',
'David',
'Gordon',
'Tony',
'John',
'Margaret',
'James'
];
The examples should all produce the same output:
- Theresa
- David
- Gordon
- Tony
- John
- Margaret
- James
document.writeln('<ol>');
for (var i = 0; i < names.length; i++){
document.writeln('<li>' + names[i] + '</li>');
}
document.writeln('</ol>');
Notice let
instead of var
, the new for...of
loop, and string templating:
document.writeln('<ol>');
for (let item of names){
document.writeln(`<li>${item}</li>`);
}
document.writeln('</ol>');
We can also take a more functional approach and shave off 2 lines:
document.writeln('<ol>');
names.forEach(item => document.writeln(`<li>${item}</li>`));
document.writeln('</ol>');
Rather than having to do string concatanation in ES5:
var numberOfMonths = 12;
var myString = 'There are ' + numberOfMonths + ' months in a year';
ES6 offers us some nice syntactic sugar to help:
const numberOfMonths = 12;
const myString = `There are ${numberOfMonths} months in a year`;
Important: This is the backtick (`) character, not a single quote.
JavaScript will automatically replace ${numberOfMonths}
with the value of numberOfMonths
for us! Magic!
There is no longer a valid use case for var
. The new const
and let
fix the issue with variables hoisted outside of blocks into function scope,
as well as a few other issues.
let
is pretty much a direct replacement forvar
and you can use it in the same waysconst
prevents variable reassignment. Keep in mind though thatconst
is notObject.freeze()
-
The following is not allowed:
const test = {foo: "bar"}; test = "hello world"; // <- will throw an error
-
However, the following is allowed:
const test = {foo: "bar"}; test.foo = "Hello World";
-
But of course plain vanilla JS doesn't scale well when working on a large app with many developers. Let's take a look at Angular and React now.
<ol>
<li ng-repeat="name in names">{{name}}</li>
</ol>
This is pretty declarative and its terseness is one of Angular's best features. Unfortunately, there's a lot of magic behind the scenes, and if you want to customize the iterator, it can be quite complex to write your own directive.
render() {
return (
<ol>
{names.map(name => {
return (<li>{name}</li>);
})}
</ol>
);
}
While not as terse as the 3 line example in function ES6 or Angular, I believe React's syntax is much better as an app's complexity increases. Notice also how we're writing JavaScript right in the middle of HTML!
In React, this is called "JSX". Files containing JSX syntax often use the file extension .jsx
.
We'll be working a lot more with JSX shortly so stick with me.
We'll implement the above example for real now.
Open src/client.jsx
in your favorite editor. Write the following:
import React from 'react';
import ReactDOM from 'react-dom';
const names = [
'Theresa',
'David',
'Gordon',
'Tony',
'John',
'Margaret',
'James'
];
window.onload = () => {
ReactDOM.render(
(<ol>
{names.map(name => {
return (<li>{name}</li>);
})}
</ol>)
, document.getElementById('main'));
};
There's a build step required to convert JSX into JavaScript that the browser can understand. To build the project, in your terminal, from the base project directory, do:
npm run build
This step uses webpack will run the React & Babel (more on Babel later) transpiler
to convert our ES6-flavored JSX into cross-browser compatible JavaScript.
I've configured it to use client.jsx
as an entry point and to output the resulting
JavaScript to src/static/js/bundle.js
. Each time you make a change to React code in this tutorial, you'll need to re-run npm run build
.
If you look at src/static/index.html
you can see that I've included our bundled JavaScript.
Let's have a look at what we have so far. In your terminal, do:
npm run start-static
and open a browser. Navigate to http://localhost:8080
and you should see the names listed out.
Important: After each code change, you will need to stop the server and re-run npm run build
and then npm run start-static
before you can see your latest changes.
One of the best features of React is how it reacts to changing application state. Rather than having to imperatively update the DOM when a user, say, adds an item to a list, React takes care of that for you because the underlying data changes.
To demonstrate that, let's take a look at an example.
Open client.jsx
again, and modify it like so:
import React from 'react';
import ReactDOM from 'react-dom';
const INITIAL_NAMES = [
'Theresa',
'David',
'Gordon',
'Tony',
'John',
'Margaret',
'James'
];
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
names: INITIAL_NAMES
};
}
render() {
return (
<div>
<ol>
{this.state.names.map(name => {
return <li>{name}</li>
})}
</ol>
</div>
);
}
}
window.onload = () => {
ReactDOM.render(<Home />, document.getElementById('main'));
};
A few things to notice here:
-
We're extending the
React.Component
class with the following line:class Home extends React.Component {
This creates a new ES6 class called
Home
and automatically gives it all of the fun stuff contained withinReact.Component
. -
The class's
constructor
is the place to put code that is run when the class is first instantiated.super(props)
tells JavaScript to call theconstructor
method of the parent class. In this case, it will callReact.Component
'sconstructor()
-
We're setting our little component with initial state with
this.state = { names: INITIAL_NAMES };
At this point, we have relinquished control over our list of names to React. We've set up the initial state, and now we let React handle it.
-
The
render()
method is relatively unchanged, except we'remap
ping over the values contained in the component's state, rather than the static names array at the top. -
In
window.onload
, we're including our<Home />
component, rather than writing therender()
method right there.
That was a lot of work for not much benefit. Let's make things dynamic so I can show off React's true power.
Modify client.jsx
once more so that it looks like this, including the console.log
. I know it's a lot, but I'll walk you through it.
import React from 'react';
import ReactDOM from 'react-dom';
const INITIAL_NAMES = [
'Theresa',
'David',
'Gordon',
'Tony',
'John',
'Margaret',
'James'
];
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
names: INITIAL_NAMES,
textboxValue: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleChange(event) {
//we're using React to manage the textbox's state
this.setState({
textboxValue: event.target.value
});
}
handleButtonClick() {
if (this.state.textboxValue === ''){
//do nothing if the textbox is empty
return false;
}
const _names = this.state.names;
_names.push (this.state.textboxValue);
//updates the component's state, including the names array and nulling out the textbox's value
this.setState({
names: _names,
textboxValue: ''
});
}
render() {
console.log('render method called. current state:', this.state);
return (
<div>
<input type="text"
value={this.state.textboxValue}
onChange={this.handleChange} />
<input type="button"
onClick={this.handleButtonClick}
value="Add Name" />
<ol>
{this.state.names.map(name => {
return <li>{name}</li>
})}
</ol>
</div>
);
}
}
window.onload = () => {
ReactDOM.render(<Home />, document.getElementById('main'));
};
After you navigate to the new page, open the developer console. Start typing in the textbox. Notice that the logging statement you added gets called after each
keystroke. React is detecting that the state changed and is updating the DOM (in render()
) to reflect that.
Don't worry, React has a very performant way of diffing and determining a minimum set of changes it needs to make to the DOM.
A complete explanation of this mechanism is unfortunately outside the scope of this tutorial, but if you're interested, here is a page explaining how it works.
Notice that we're using native JS events to tell React what to do (onChange
, onClick
). We point them to methods on the class, handleChange()
and handleButtonClick()
, respectively.
In the class's constructor
method, we need to bind this
to these functions.
this.handleChange = this.handleChange.bind(this);
If we don't, this
refers to the class definition instead of an instance, as we would expect.
React is fairly opinionated when it comes to the layout of code. Components are a way to package UI features into reusable and relatively atomic portions. Let's componentize some of the example above.
One obvious choice for componentization is the textbox and button. We should make that bit of UI a component for a few reasons:
- It could be reused other places in our app, any time we wanted a textbox and a button
- We can more easily reason what that bit of code does, because its inputs and outputs are restricted
- It makes the code more maintainable and readable
Let's rip out the two <input>
tags and replace them with a component!
Once again, edit client.jsx
:
import React from 'react';
import ReactDOM from 'react-dom';
const INITIAL_NAMES = [
'Theresa',
'David',
'Gordon',
'Tony',
'John',
'Margaret',
'James'
];
class NameInput extends React.Component {
constructor(props) {
super(props);
this.state = {
textboxValue: ''
};
this.handleChange = this.handleChange.bind(this);
this.handleButtonClick = this.handleButtonClick.bind(this);
}
handleChange(event) {
//we're using React to manage the textbox's state
this.setState({
textboxValue: event.target.value
});
}
handleButtonClick() {
if (this.state.textboxValue === ''){
//do nothing if the textbox is empty
return false;
}
//calls the onAddValue function passed to this component as a prop
//we pass it as an argument so that the parent component can handle the new value.
this.props.onAddValue(this.state.textboxValue);
//now we null out this component's textboxValue to erase it.
this.setState({
textboxValue: ''
});
}
render() {
return (
<div>
<input type="text"
value={this.state.textboxValue}
onChange={this.handleChange} />
<input type="button"
onClick={this.handleButtonClick}
value={this.props.buttonText} />
</div>
);
}
}
class Home extends React.Component {
constructor(props) {
super(props);
this.state = {
names: INITIAL_NAMES
};
this.handleAddValue = this.handleAddValue.bind(this);
}
handleAddValue(value) {
const _names = this.state.names;
_names.push(value);
//updates this component's state
this.setState({
names: _names
});
}
render() {
console.log('render method called. current state:', this.state);
return (
<div>
<NameInput buttonText="Add a new value" onAddValue={this.handleAddValue}/>
<ol>
{this.state.names.map(name => {
return <li>{name}</li>
})}
</ol>
</div>
);
}
}
window.onload = () => {
ReactDOM.render(<Home />, document.getElementById('main'));
};
Some more big changes here:
-
We've broken out the
<input>
tags into a new classNameInput
:class NameInput extends React.Component {
Notice also that we've changed the
render()
method ofHome
to no longer have the textbox and button, but rather:<NameInput buttonText="Add a new value" onAddValue={this.handleAddValue} />
-
The methods
handleChange()
andhandleButtonClick()
have been moved to theNameInput
class. With a few exceptions, (unfortunately outside the scope of this tutorial), atomic components like this should be responsible for handling their own state. -
The button's text is being provided to
NameInput
by it's parent,Home
. When we includeNameInput
:<NameInput buttonText="Add a new value" ...
we are passing the text we want for the button as a prop. You can see that in
NameInput
, the button's code is:<input type="button" onClick={this.handleButtonClick} value={this.props.buttonText} />
We set the
value
of the<input>
to bethis.props.buttonText
. -
Our new
NameInput
component will be responsible for handling the state changes of user input, and only when the user clicks the "Add Name" button, will it report to its parent (Home
) that a new name should be added to the list. -
When
NameInput
says it wants to add a new name to the list, it callsthis.props.onAddValue()
method. The handler for this method is withinHome
,handleAddValue()
. That handler is passed from parent to child in the exact same way that we dictated the button text.<NameInput buttonText="Add a new value" onAddValue={this.handleAddValue} />
For the purposes of this contrived example, I added both components to the same file. In reality, each component should have its own file. This will be demonstrated shortly.
Now that you have a basic understanding of React's state
and props
, let's start building the video player!
(The red lines denote the boundaries of React components.)
- Layout.jsx: The wrapper parent component that contains the header and body of our app.
- PlayerSurface.jsx: This component is the body of our app. It houses the video and the thumbnail picker, as well as acting as the controller.
- Video.jsx: Contains the logic required to render the
<video>
element - VideoPicker.jsx: This component handles video selection. It includes
Thumbnail
s andScrollButton
s and controls the logic for their selection. - Thumbnail.jsx: Straightforward component that displays an image and offers a click handler.
- ScrollButton.jsx: Simple button wrapper that tells
VideoPicker
to scroll left or right.
We'll take things a bit further by making this a single page app.
When a user selects a different video, we'll update the URL in the browser. Updating the URL will trigger the video to change.
In this app, this provides a streamlined way to share the URL to friends and send them directly to the video:
/video/5
, where "5" is the ID of the video. This URL change will not require a page reload, so that's why it's called a "single page app".
React Router is a routing library built for React. It follows React's component-based architecture very well, and so it makes perfect sense to use for this example app. Once we set it up, it should be straightforward to add new routes as we need.
Let's get started.
Within src/
make a new file called Routes.jsx
and open it in your editor. Add:
import React from 'react'
import { Route, IndexRoute } from 'react-router'
import Layout from './components/Layout.jsx';
import PlayerSurface from './components/PlayerSurface.jsx';
import NotFound from './components/NotFound.jsx';
const routes = (
<Route path="/" component={Layout}>
<IndexRoute component={PlayerSurface}/>
<Route path="video" component={PlayerSurface} />
<Route path="video/:id" component={PlayerSurface} />
<Route path="*" component={NotFound}/>
</Route>
);
export default routes;
-
With ES6, we can import just portions of a file with the syntax: (more info here)
import {foo, bar} from "my-module";
-
Besides importing react and react-router, we're importing
Layout.jsx
PlayerSurface.jsx
NotFound.jsx
(for our 404 route)
We'll make these files soon.
-
The first bit of JSX defines the layout we're going to use for our app:
<Route path="/" component={Layout}>
-
The first child of
<Route>
defines the index path (/
). When a user lands on our site with no path, this is the page we'll render.<IndexRoute component={PlayerSurface}/>
-
The same goes for
/video
. We'll render thePlayerService
component. -
We want to use React router to pass the video ID from the path to our app, and we do that with the Express-like syntax:
video/:id
. Anything that is given aftervideo/
will be passed to our app, and the app will decide how to handle it.<Route path="video/:id" component={PlayerSurface} />
-
The last
<Route>
matches for anything other than/
,/video
, and/video/:id
. We'll render our 404 page in this case.
One last bit of react-router config. In src/components
, make & open AppRoutes.jsx
:
import React from 'react';
import { Router, browserHistory } from 'react-router';
import routes from '../routes.jsx';
export default class AppRoutes extends React.Component {
render() {
return (
<Router history={browserHistory} routes={routes} />
);
}
}
- This is the entry point for react-router. We're configuring it to use the vanilla browserHistory API and to use the routes we just created in
Routes.jsx
.
That's it! react-router will handle the rest of the heavy lifting for us. We'll be able to see it working soon, so stay with me.
Most web app frameworks require a layout template on which to build your app. This app is no different. Within /src/components
, make Layout.jsx
and open it in your editor.
import React from 'react';
import { Link } from 'react-router';
export default class Layout extends React.Component {
render() {
return (
<div id="layout" className="layout">
<header>
<Link to="/">
<span class="logo">Home</span>
</Link>
</header>
<div className="content">
{this.props.children}
</div>
</div>
);
}
}
-
We're importing
Link
from react-router. This is the easiest way to add links between routes in React. -
Our
<header>
is static in this case, but it doesn't have to be. More complex apps could have an entire navigation component included here, for example. -
Notice
{this.props.children}
. Earlier, inRoutes.jsx
, you saw that we had components nested inside other components, just like real HTML. The same goes with React. Components nested in this fashion are passed to their children, and accessible viathis.props.children
. The developer is responsible for implementing rendering them.
We'll be coming back to this file later, but for now, in src/components
, create PlayerSurface.jsx
and open it up. For now:
import React from 'react';
import { Link } from 'react-router';
export default class PlayerSurface extends React.Component {
render() {
return (
<div>
<div>Hello world!</div>
<Link to="/video">Go to /video</Link>
<Link to="/video/1">Go to /video/1</Link>
<Link to="/video/2">Go to /video/2</Link>
<div>The value of :id is {this.props.params.id}.</div>
</div>
);
}
}
- Notice how I'm referencing
this.props.params.id
. Theparams
object contains parameters passed in the path, and we can do anything we want with the params now, including just writing them to the page.
Our 404 page is simple enough. Create & open src/components/NotFound.jsx
:
import React from 'react';
import { Link } from 'react-router';
export default class NotFound extends React.Component {
render() {
return (
<div className="not-found">
<h1>404 - Not Found</h1>
<Link to="/">Click here to return home.</Link>
</div>
);
}
}
- Notice how we provide a
<Link to="/">
to allow the user an easy way home.<Link>
is a react-router way of linking to different pages.
We have now done almost enough work to stand up our very first React web app. Sure it doesn't do much, but we'll enhance it soon.
The last thing to do is to modify src/client.jsx
to use our new router:
import React from 'react';
import ReactDOM from 'react-dom';
import AppRoutes from './components/AppRoutes.jsx';
window.onload = () => {
ReactDOM.render(<AppRoutes/>, document.getElementById('main'));
};
- This tells React to render
<appRoutes/>
in the main area of our HTML.
Now that that's done, rebuild the project and run it:
npm run build
and
npm run start-static
Navigate to localhost:8080
to (hopefully) see "Hello World!" If that works, try clicking the <Link>
s to see how the params are written to the page.
Now that we've stood up a functional app, let's start making it useful. One by one, we'll add more components to PlayerSurface.jsx
until it resembles the example image above.
We'll start with Video.jsx
. Create it in src/components
and open it.
import React from 'react';
export default class Video extends React.Component {
constructor() {
super();
this.togglePlayState = this.togglePlayState.bind(this);
}
togglePlayState() {
if (this.video.paused){
this.video.play();
} else {
this.video.pause();
}
}
componentDidUpdate(previousProps, previousState){
if(previousProps.source !== this.props.source){
/*
We need to imperatively call .load() here because while React's render() will update the
<source> within <video>, <video> will not reload automatically.
*/
this.video.load();
}
}
render() {
const TYPE = 'video/mp4';
return (
<div className="video-wrapper" onClick={this.togglePlayState}>
<h3>{this.props.title}</h3>
<video controls height="700" width="1200"
poster={this.props.poster}
ref={(ref) => {this.video = ref;}}>
<source src={this.props.source} type={TYPE} />
{/* Fallback text for browsers that don't support HTML5 playback... */}
Your browser does not support HTML5 Video playback
</video>
</div>
);
}
}
I'll start with render()
and work my way backwards:
-
Notice the
onClick
handler on the video wrapper. This is to play/pause the video when the user clicks anywhere on the video surface. -
The
<h3>
wraps the selected video's title. -
This app uses the HTML5
<video>
tag. If you're not familiar with it, W3 Schools offers a nice tutorial. For now, I'll walk you through it. -
<video>
has a few attributes:controls
: Whether or not the browser should render video controls for the videoheight
,width
poster
: A placeholder image to use while the video is loading, is paused, or finishes.ref
: This isn't a built-in HTML attribute. It's very powerful though, and I'll get to it in a minute.
-
Within
<video>
, n<source>
tags are added to specify the possible video files to use. You might use more than one<source>
when specifying different filetypes, for example. For our little example, we'll just use one. -
The plain text following the
<source>
elements is used if the browser doesn't support video playback. Most do, but to be safe, we include it here.
-
This is an example of a React "Lifecycle Method". Others will be introduced later.
-
This function is called after React has reacted to a change of state and has re-rendered (if it needed to). It provides the previous state's props and states, so we can do some imperative logic with them.
-
In this case, we're using it to check if the source changed from what it used to be. If it has, this means that the user has selected a different video
-
We then imperatively call
<video>
'sload()
method to tell it to fetch the video asset. -
Important: Notice how we're able to access the
<video>
element directly. This is because in therender()
method, we assigned the<video>
aref
.
It's often important to have a reference to a DOM element directly. While in general you should use React's way of reacting to state to drive changes in your application, there might be times where you simply cannot avoid working with the element itself. One example of this is when you need to call functions on a DOM element.
Here's how this works:
<video ref={
(ref) => {this.video = ref;}
}></video>
ref
allows you to pass a callback that gets called when the component is mounted. It provides a reference to the element as an argument.
We can then use this reference to add it to our React component via this.video = ref;
After it's mounted & our element is now added to our React component object, we can reference it directly by
-
This is an event handler for when the user clicks on the video surface.
-
We're just checking if the video is paused, and if it is, we
play()
it, otherwise wepause()
it.
- This is the same as other constructor methods on other classes, except we need to be sure to:
We have to bind
this.togglePlayState = this.togglePlayState.bind(this);
this
to thetogglePlayState
method. If we don't, when we go to call the method, it will refer to a static method that is shared between all instances of ourVideo
class, not the instance we're concerned with. Whenconstructor()
gets called, it will bindthis
to the method:this
being a reference to the current instance.
Go back and open src/components/PlayerSurface.jsx
. Enter the following:
import React from 'react';
import Video from './Video.jsx';
import videos from '../data/data.js';
export default class PlayerSurface extends React.Component {
constructor(props) {
super(props);
//we'll use the class's constructor to define this component's initial state
const videoIdFromRouter = this.props.params.id;
this.state = {
selectedVideo: this.getVideoById(videos, videoIdFromRouter)
};
}
componentWillReceiveProps(nextProps) {
//note: this method is not called for the initial render
//we'll use it here to react to videoId changes from React Router
const videoIdFromRouter = nextProps.params.id;
//setState() causes React to rerender
this.setState({
selectedVideo: this.getVideoById(videos, videoIdFromRouter)
});
}
getVideoById(videos, videoId) {
let foundVideo;
if (videoId) {
//look for the video in our data
foundVideo = videos.find(item => {
//I used weak `==` in case '5' or 5, for example
return item.id == videoId;
});
}
if (!foundVideo){
//pick the first one
foundVideo = videos[0];
}
return foundVideo;
}
render() {
const source = this.state.selectedVideo.video.url;
const poster = this.state.selectedVideo.heroUrl;
const title = this.state.selectedVideo.title;
return (
<div className="player-surface">
<input type="button" value="Test!" onClick={() => {
//test method
const selection = prompt('Which video?');
this.setState({
selectedVideo: this.getVideoById(videos, selection)
});
}} />
<Video source={source}
poster={poster}
title={title} />
</div>
);
}
}
I'm going to start from the top of the file this time:
- Notice we're
import
ing our newVideo
React component, as well as our data module.
-
We're passing
props
as an argument tosuper()
so that the parent class can have access to this component's props. -
constructor()
is an ideal place to set up a component's initial state.-
We get the
id
from react-router by accessing it here:const videoIdFromRouter = this.props.params.id;
-
Now that we have the video's ID, we'll pass it to our function
getVideoById()
to retrieve the corresponding data map, and we set the component's state:this.state = { selectedVideo: this.getVideoById(this.props.videos, videoIdFromRouter) };
-
This method accepts our data map and retrieves the entry that corresponds to videoId
.
If we provide it an ID it doesn't recognize, it returns the first video.
(If we were shipping this app to production, we might want to have some error handling instead of just selecting the first video.)
This is another "Lifecycle Method", and this particular one gets called when a parent component changes a component's props.
It provides nextProps
as an argument, which is the future state of our component's props. To access the current props. Use this.props
as usual.
Here we're using it to react to react-router changing the ID when we navigate to a new path.
-
First, I'm storing values with a long path in
const
s to make the JSX appear cleaner. This is just a style thing, and it's not at all required. -
Within the wrapper
<div>
, I've created a quick test button that will allow us to switch videos without a video picker. Notice thatthis
means the react component. Had I written this function in ES5, it would look like:onClick = { function() { //test method var selection = prompt('Which video?'); this.setState({ selectedVideo: this.getVideoById(videos, selection) }); }}
but
this
would not refer to the React component, but rather the click handler function. We'd have to usebind(this)
to get the correct value ofthis
. However, with ES6 arrow function syntax,this
is the value we expect. -
We're then including our
<Video>
component and passing the props it expects.
We've now got enough written to test it out again! Do:
npm run build
and
npm run start-static
and go to localhost:8080
. The first video should be selected, and it should be playable.
After you confirm that it plays, click the test button and enter a new number for the video ID (check data/data.js
for possible values.)
We've got a working video player and that's great, but having a prompt()
box isn't the prettiest of user interfaces.
Let's enhance it with a horizontal scrolling list of videos.
The first step is to make a Thumbnail component that is responsible for rendering an image, displaying the video title, and passing a click handler to its parent.
Create & open src/components/Thumbnail.jsx
:
import React from 'react';
import classNames from 'classnames';
export default class Thumbnail extends React.Component {
render() {
const classes = classNames('thumbnail-wrapper', {
'thumbnail-clickable': !!this.props.onThumbClick,
'thumbnail-active': this.props.isActive
});
const divStyle = {
backgroundImage: `url(${this.props.imgUrl})`
};
//notice the arrow function which allows us to pass this.props.id to the click handler function
return (
<div className={classes}
title={this.props.title}
onClick={() => {this.props.onThumbClick(this.props.id)}}
style={divStyle}>
<span className="thumbnail-title">{this.props.title}</span>
</div>
);
}
}
I'll start top-down again.
- We're importing the classnames package. This is a super useful library that affords us a little more power when adding CSS classes to React components.
- classnames has a neat interface. The first argument can be a string or an object, and the second argument can be an object or nothing.
- When we pass a string as an argument, it will set it up such that that class is always present on the element.
- When passing an object, you're allowed a little more flexibilty. Classes will be added to components if their values are truthy:
thumbnail-clickable
will only be applied if a click handler is passed as a prop to this component. If there's no click handler passed toThumbnail
, then the CSS classthumbnail-clickable
will not be applied.thumbnail-active
will be applied ifthis.props.isActive
is truthy.
- Since we're going to set a background image on this element, we need to use JS to set it imperatively. Working with styles in React isn't difficult at all.
- Notice that I use
backgroundImage
instead ofbackground-image
like you might expect in CSS. This is because React is working with the underlying DOM element and not with CSS. Prove this to yourself by inspecting the contents of a native DOM element'sstyle
property. **Anything you're used to that uses kebab case (for example,this-is-kebab-case
), you will need to convert to camel case (thisIsCamelCase
).
- Notice that I use
- We're now rendering a
<div>
and passing it:- The
classes
object created by classnames - A title
- A click handler
onClick={() => {this.props.onThumbClick(this.props.id)}}
. Notice that we're using ES6 arrow syntax. This allows us to pass an argument to the click handler, in this case, the thumbnail's video ID. - The style we created above that sets the correct background image.
- The
- We're also rendering a
<span>
that holds the video's title and that is positioned over the thumb via CSS.
We need a place to put all those Thumbnail
s we have. Create and open src/components/VideoPicker.jsx
:
import React from 'react';
import ScrollButton from './ScrollButton.jsx';
export default class VideoPicker extends React.Component {
constructor() {
super()
this.scrollClickHandler = this.scrollClickHandler.bind(this);
}
scrollClickHandler(direction) {
const SCROLL_AMOUNT = 200;
//the this.thumbnails now refers to the dom element itself, '.video-picker-thumbnails'
const currentScrollLeft = this.thumbnails.scrollLeft;
let modifier;
if (direction === 'left'){
modifier = -1;
} else if (direction === 'right') {
modifier = 1;
}
//will scroll the element left or right, depending on modifier
this.thumbnails.scrollLeft = currentScrollLeft + (modifier * SCROLL_AMOUNT);
}
render() {
return (
<div className="video-picker">
<ScrollButton direction="left" onScrollClick={() => {this.scrollClickHandler('left');}} />
{/* refs are a way to store a reference to the DOM node for later use */}
<div className="video-picker-thumbnails" ref={(ref) => { this.thumbnails = ref;} }>
{this.props.children}
</div>
<ScrollButton direction="right" onScrollClick={() => {this.scrollClickHandler('right');}} />
</div>
);
}
}
- We're
import
ing a not-quite-built-yet component ScrollButton. We'll get there very soon.
- Not too much to see here, other than that we're binding
this
to the value we expect forscrollClickHandler()
.
While we haven't made it yet, we will make horizontal scrolling buttons soon. This is the handler that is invoked when a user clicks on one.
The JavaScript here is fairly straightforward, but do notice the following line:
this.thumbnails.scrollLeft = currentScrollLeft + (modifier * SCROLL_AMOUNT);
We are again using ref
to help us get a reference to a dom element, since we need to work directly with the DOM to access scrollLeft
.
- After our wrapper
<div>
, we render the<ScrollButton/>
we're about to make. Notice we pass it a direction and a click handler as props. - We then render a wrapper
<div>
, and useref
to allow us to refer to the element inscrollClickHandler()
. - Inside the wrapper
<div>
, we render the component's children. - After that, we render another
<ScrollButton/>
, this time a rightward one.
The end is near! Let's quickly write a scroll button component.
Create and open src/components/ScrollButton.jsx
:
import React from 'react';
import classNames from 'classnames';
export default class ScrollButton extends React.Component {
render() {
const classes = classNames('scroll-button', {
//there may be terser ways to express this, but I wrote it out here for clarity
'scroll-button-left': this.props.direction === 'left',
'scroll-button-right': this.props.direction === 'right'
});
return (
<input type="button"
className={classes}
value={this.props.direction}
onClick={this.props.onScrollClick} />
);
}
}
This isn't a big component at all. The < and > arrows are created using some CSS hackery, so we don't have to worry about icons.
-
Notice how we're using classnames again to selectively add a CSS class to our component based on the
direction
prop passed to ScrollButton. -
We then render a button and pass along:
- type
- className (from classnames)
- value (not really needed but buttons need some value.)
- onClick, which is just calling the click handler passed to ScrollButton by its parent.
We've now written all the components we need to make a fully functional video player in React!
Go back and open src/components/PlayerSurface.jsx
. Replace the contents for:
import React from 'react';
import {browserHistory} from 'react-router';
import Video from './Video.jsx';
import VideoPicker from './VideoPicker.jsx';
import Thumbnail from './Thumbnail.jsx';
import VIDEOS from '../data/data.js';
export default class PlayerSurface extends React.Component {
constructor(props) {
super(props);
//we'll use the class's constructor to define this component's initial state
const videoIdFromRouter = this.props.params.id;
this.state = {
selectedVideo: this.getVideoById(this.props.videos, videoIdFromRouter)
};
}
componentWillReceiveProps(nextProps) {
//note: this method is not called for the initial render
//we'll use it here to react to videoId changes from React Router
const videoIdFromRouter = nextProps.params.id;
//setState() causes React to rerender
this.setState({
selectedVideo: this.getVideoById(this.props.videos, videoIdFromRouter)
});
}
getVideoById(videos, videoId) {
let foundVideo;
if (videoId) {
//look for the video in our data
foundVideo = videos.find(item => {
//I used weak `==` in case '5' or 5, for example
return item.id == videoId;
});
}
if (!foundVideo){
//pick the first one
foundVideo = videos[0];
}
return foundVideo;
}
handleThumbClick(videoId){
/*
use react router to change the selected video.
the change will eventually be picked back up here in this class in
componentWillReceiveProps(...)
*/
browserHistory.push(`/video/${videoId}`);
}
renderThumbnails(videos) {
return videos.map((video, idx) => {
return (
<Thumbnail imgUrl={video.thumbnailUrl}
title={video.title}
id={video.id}
onThumbClick={() => {this.handleThumbClick(video.id);}}
isActive={video.id === this.state.selectedVideo.id}/>
);
});
}
render() {
const selectedVideoSource = this.state.selectedVideo.video.url;
const poster = this.state.selectedVideo.heroUrl;
const title = this.state.selectedVideo.title;
return (
<div className="player-surface">
<Video source={selectedVideoSource}
poster={poster}
title={title} />
<VideoPicker>
{this.renderThumbnails(this.props.videos)}
</VideoPicker>
</div>
);
}
}
PlayerSurface.defaultProps = {
videos: VIDEOS
};
There are a few new things happening:
This is the click handler that we pass to a <Thumbnail/>
component. Inside the function, we're using the browser history API to push a new state to the history.
react-router will then react to that state change and our React components will follow suit.
This function is used to abstract some logic away from the render()
method and make it more readable. It's best practice to chunk up separate logic like this into smaller functions.
- The function provides us a
videos
object, which we thenmap()
over, returning a new<Thumbnail />
component for each element invideos
. - We're passing various props to it as well. You should be familiar enough with props now to understand this, but I do want to point out:
onThumbClick
is passing the video ID via ES6 arrow function syntax.isActive
is true when the currently playing video's ID is equal to the item of iteration
We render the <Video/>
just as before, no changes there, but we are adding:
<VideoPicker>
{this.renderThumbnails(this.props.videos)}
</VideoPicker>
This renders our VideoPicker component and passes the result of renderThumbnails()
as children to VideoPicker.
Sometimes, you may find that you need to set default props on a component when it first renders.
These default props would get overridden by a parent component passing props down.
defaultProps = {}
allows you to set default props on a component. This is another really powerful feature of React, and enables even more code reuse and extensibility.
In a real app, you'd likely have an entire data access layer to fetch your data for you. For this contrived video player example, we don't.
PlayerSurface.defaultProps = {
videos: VIDEOS
};
Here we're simply setting this.props.videos
to VIDEOS
, which is our static data that we import
ed at the top of the file. Now our file will be hydrated with some nice video data.
Note: When you extend an ES6 class this way, it's critical that it goes after the class declaration. Unlike functions and variables in JavaScript, classes are NOT hoisted.
Run your project by doing npm run build
and npm run start-static
and try it out!
Congrats, we've now got a fully functional client-side video player webapp!
We have a problem though. In the address bar in your browser, type localhost:8080/video/3
and hit enter.
We expect it to go to the fourth video in our list, but instead we get nothing!
Luckily a solution is on the horizon. What's wrong is that we don't have a server that knows how to interpret the route /videos/3
. Sure, the client can, but http-server
is looking for that route in vain.
We need a server to solve our problems.
Since React doesn't directly work with the real DOM, but instead uses a virtual, in memory construction of one, there's nothing stopping us from pre-rendering React on a server & shipping the rendered code to the client. This gives the client a performance boost, since there's no need to wait for React to bootstrap itself. For simple static and multi-page, form-based apps, this means that you might not even need to send JS to the client at all!
We're using node and Express for this tutorial. Since this is a React and ES6 workshop and not a NodeJS one, I'm going to move quickly through the explanation.
Take a quick look at src/views/index.ejs
. This is very similar to our index.html
from earlier, but we're using a templating markup:
<div id="main"><%- markup -%></div>
In the server code, we'll replace <%- markup -%>
with our main React component.
Rename src/static/index.html
to something else, otherwise the Express server will attempt to serve it instead of doing server-side rendering.
Open src/server.js
in your editor. The magic happens on line 39:
markup = renderToString(<RouterContext {...renderProps}/>);
renderToString
uses React to render HTML for us. Later on, on line 47:
return res.render('index', { markup });
We send the rendered markup down to the client, replacing the template placeholder in index.esj
.
That's pretty much it. It takes ~60 lines of code (it would be a lot less without whitespace and comments) to render a universal React webapp!
Let's give it a spin. In your terminal, do:
npm run start
The server should start up, running off of port 3000. In your browser, hit localhost:3000
.
The app's functionality should be identical, but you can now also change the current video by changing the video ID in the URL. Turn off JavaScript in your browser and load the page. While it's not interactive (it takes JS to run), it does completely render.
-
To Luciano Mammino (Twitter) for his wonderful article React on the Server for Beginners: Build a Universal React and Node App" for refreshing my memory on how to make a universal JS webapp from scratch.
-
To Brian Holt (Twitter) for letting me TA for him on this workshop (from which I borrowed some ideas), and for encouraging me to give workshops on my own.
-
Video and image assets used in this demonstration were obtained from archive.org.
2016 Tony Edwards (Twitter)