Build And Share Your Single-page App With React, Node and Pouchdb, part II
Table of contents
- Description of the main concepts of single page applications
- How to build your client with ReactJS and Browserify
- How to build your REST API with ExpressJS, Americano and PouchDB
- Share and deploy your web app with NPM
How to build your client with ReactJS and Browserify
In the first part, we mentioned the motivations behind this tutorial. Then, we reminded you what is a Single-Page App and which techonologies will be involved. Now, we are going to discuss the development of your single page application.
The application we chosed for this tutorial is a bookmark manager. It will allow you to manage a bookmark list (to visualise, create and delete bookmarks).
To make our app we need to build the frontend part (the User Interface) and the server part (the API that will allow the frontend to access data).
In this chapter we'll discuss only the frontend part. For that, we'll introduce you to three technologies:
- ReactJS: to handle the rendering and the management of our components.
- Browserify: to assemble properly our Javascript files.
- Superagent: to request the API
NB: We decide to start with UI first. We want to build our app from the user point of view, not the data point of view.
User stories
Before jumping into the code and the configuration, let's describe the main requirements that will be covered by our app:
- As a user, I want to be able to create new bookmarks.
- As a user, I want to visualize my bookmark list.
- As a user, I want to be able to delete a given bookmark.
- As a developer, I should have a simple way to ship my frontend.
Now let's break this down in more precise user stories:
- When I load the app, I can see the bookmarks stored in my app database.
- When I load the page, I see a a text and an input inviting me to add a link.
- When I fill the input and click on the save button, it adds the bookmark to the list and save it in the database.
- When I click on a bookmark, it opens a new tab and loads the clicked link.
- When I click on the delete button of a bookmark, it removes the bookmark from the list and delete it in database.
- When I change a file, the app builder is run automatically.
- When I build my app, I obtain a single minified file with all my javascript code.
Client File Tree
Let's organize the code. For this app, we'll keep the file tree super simple.
client/public/index.css
client/public/index.html
client/src/index.js
client/package.json
It contains a manifest file dedicated to the client.
The index.html
file will be the entry point of your app. It will load style and script files. index.css
will contain the styles of the application. Finally index.js
will contain the logic of our frontend.
NB: what you have to keep in mind is that you can add as many folder and files as you want in the src
folder. Browserify will do the job of making a single file from them that will be located in the public
folder.
Entry Point
The entry point is the HTML file that will be loaded by the server everytime a user connects to our app (it's the index.html
file mentioned before). It will load the app Stylesheet and the Javascript code that will build the interactive user interface. Here is how it looks:
<html>
<head>
<title>My bookmarks</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="app">
Loading...
</div>
<script src="browser-bundle.js"></script>
</body>
</html>
We kept it simple. In the header, we give a title to the app page and say which file should be use to change the app. In the body of our html code, we'll set a div
tag that is required by the React app to be loaded. Then, we add the script
tag that will load our Javascript code.
By default the div
tag contains a loading text. It's useful because it's displayed until the React app and the data are loaded. It prevents the user to see a blank screen.
Styles
Styling and design is not the subject of this tutorial so we just set one CSS rule, just to show you that styles can be included too. Here we ensure that buttons have the right pointer when the mouse is over them.
button { cursor: pointer; }
Browserify
Browserify is the technology that will allow us to build properly our app by:
- Managing app dependencies through NPM, the Node.js package manager.
- Making one file from all our Javascript files (dependencies included).
- Minify our build for production purpose.
As you noticed, Browserify does most of the work. The only thing we have to do is to provide him with a proper manifest describing our dependencies and our app.
{
"name": "my-bookmarks-client",
"version": "0.0.1",
"description": "Client for My Bookmarks application.",
"main": "src/index.js",
"dependencies": {
"react": "0.13.1",
"slug": "0.8.0",
"superagent": "1.1.0"
},
"devDependencies": {
"browserify": "9.0.3",
"envify": "3.4.0",
"reactify": "1.1.0",
"uglify-js": "2.4.17",
"watchify": "2.5.0"
},
"scripts": {
"start": "node_modules/.bin/watchify src/index.js -o ./public/browser-bundle.js -v -d .",
"build": "NODE_ENV=production browserify . | uglifyjs -cm > ./public/browser-bundle.min.js"
},
"author": "Cozy Cloud",
"license": "MIT",
"browserify": {
"transform": [
"reactify",
"envify"
]
}
}
We are going to only describe the important fields of this manifest. All fields non-mentioned below are easy to understand. That's why we won't cover them.
Main file
"main": "src/index.js",
It's the file that will start the React app. It's the entry point for the JS build. From there all the required dependencies and modules will be grabbed to build a single js file.
Dependencies
"dependencies": {
"react": "0.13.1",
"slug": "0.8.0",
"superagent": "1.1.0"
},
It's the list of dependencies that should be downloaded from the NPM repository. You can search for available package here. Because Node.js is written in Javascript libs can be shared among client and server.
We'll need React, Slug (a lib to make strings more machine compliant) and Superagent as dependencies. As you can see a version number is required too. Now we are done with the manifest, we can download the dependencies that will be stored in a node_module
folder. Let's run the following command:
cd client
npm install
NB: We assume you have installed Node.js before
Dev Dependencies
"devDependencies": {
"browserify": "11.2.0",
"reactify": "1.1.1",
"uglify-js": "2.4.24",
"watchify": "3.4.0"
},
They are the libs required to make browserify operational. reactify
is only needed for React projects. uglify-js
will handle the minification and watchify
will be used for automatic build after a change occurs.
Moreover browserify will require a little bit more of configuration to work with React properly. We need to tell it that React files requires some operations before the build:
"browserify": {
"transform": [
"reactify"
]
}
Commands
"scripts": {
"build": "NODE_ENV=production browserify . | uglifyjs -cm > ./public/browser-bundle.min.js"
"start": "node_modules/.bin/watchify src/index.js -o ./public/browser-bundle.js -v -d .",
},
User story covered: When I build my app, I obtain a single minified file for all my javascript code.
Here we define two commands. The first command will allow you to turn your javascript files in a single minified file. When your manifest is properly set you can run the following command to build your production file:
npm run-script build
As you noticed, to set a command you have to put its name as key and the corresponding bash script as value. To run it make sur you index.js
file exists or this command will fail telling that the module doesn't exist.
User story covered: When I change a file, the app builder is run automatically.
The second command allow to start watching file changes and rebuild automatically the app when a change occurs. It will be very useful during the development. So before going further, run (it doesn't require the run-script prefix because it's a standard command of npm):
npm start
React
Now let's jump to the client code. The first thing is to create the main component of your application. With React you have an utility method that allows you to create component class, it's called createClass
. That one will allow you to build a component. That component contains one function called render that will allow you to describe the content of the component. It's based on JSX code, it's a template language you can put in the middle of your Javacript code. Here is a very simple component with only a title.
var React = require('react');
var App = React.createClass({
render: function() {
return (
<div>
<h1>My Single Page Application</h1>
</div>
)
}
});
Thanks to Browserify we require our dependencies like we were in Node.js.
Then we have to render our app. It's done by JSX code again and a call to the main render
class method of React object. JSX template is given as first parameter. The second parameter is the DOM element that will host your component. Here it's our div
of which id is equal to "app".
React.render(<App> </App>, document.getElementById('app'));
Once done, you should already be able to see the result by opening the index.html
file in your browser.
Now, we want to display our bookmark list. First let's display two bookmarks by adding two bookmark components to the main JSX components.
To handle properly the rendering , the App
component requires the bookmark data. It needs an attribute that contains the list of bookmarks to render.
var App = React.createClass({
render: function() {
var bookmark = this.props.bookmarks[0];
var bookmark2 = this.props.bookmarks[1];
return (
<div>
<h1>My Single Page Application</h1>
<Bookmark title={bookmark.title}
link={bookmark.link}>
</Bookmark>
<Bookmark title={bookmark2.title}
link={bookmark2.link}>
</Bookmark>
</div>
)
}
});
bookmarks = [
{title: "title 01", link: "http://bookmark.com/"},
{title: "title 02", link: "http://bookmark2.com/"}
]
React.render(<App bookmarks={bookmarks}></App>,
document.getElementById('app'));
User story covered: When I click on a bookmark, it opens a new tab and loads the clicked link.
Let's now describe the bookmark component. We want one div with two paragraphs that will contain the title of the link and the hypertext link of the bookmark. With React, parameters of a component are stored in the props field of the component object.
var Bookmark = React.createClass({
render: function() {
return (
<div>
<p class="title">{this.props.title}</p>
<p class="link">
<a href={this.props.link} target="_blank">
{this.props.link}
</a>
</p>
</div>
);
}
});
Now go back to your index file by refreshing your browser. Things should render properly and you should see two bookmarks displayed. An hyperlink is displayed so you can click on it to visit the bookmarked url.
User story covered: When I load the app, I can see the bookmarks stored in my app database.
Let's jump to the bookmark list component. This component has a particularity. It requires to define a state, a javascript object that will store information about what will be rendered. Every time the reference to this element change, the component is rendered again. So we will be able to take advantage of it to change the rendering by modifying state data.
To set the state properly, we have to define a getInitialState
function.
var BookmarkList = React.createClass({
getInitialState: function() {
return {bookmarks: this.props.bookmarks};
},
The second change occurs in the render method. Here we take into account the list of bookmarks set in the state. It will allow us to change the bookmark list display interactively by changing the bookmark list. For that, we run a for loop that will build a component for each bookmark.
render: function() {
var removeLine = this.removeLine;
var bookmarks = this.state.bookmarks.map(function(bookmark) {
return (
<Bookmark title={bookmark.title} link={bookmark.link}>
</Bookmark>
);
});
return (
<div>
<div>
{bookmarks}
</div>
</div>
);
}
});
Then we change the main component to use our bookmark list:
var App = React.createClass({
render: function() {
return (
<div>
<h1>My Single Page Application</h1>
<BookmarkList bookmarks={this.props.bookmarks}>
</BookmarkList>
</div>
)
}
});
User story covered: When I load the page, I see an a text and an input inviting me to add a link.
It's the opportunity too to add a widget that will allow to add a bookmark. We add into the list template:
return (
<div>
<div>
<label>title</label>
<input ref="titleInput" type="text" ></input>
</div>
<div>
<label>url</label>
<input ref="linkInput" type="text"></input>
</div>
<div>
<button onClick={this.onAddClicked}>+</button>
</div>
<div>
{bookmarks}
</div>
</div>
);
User story covered: When I fill the input and click on the save button, it adds the bookmark to the list and save it in the database.
The add button will fire the onAddClicked
function when it will be clicked. Here we grab the value entered in the title and link field. Then, we add a bookmark object to our bookmark list. Finally we ask to React to change the state of the component with our new bookmark list. It will cause a new render of the bookmark list component.
onAddClicked: function() {
var bookmarks = this.state.bookmarks;
var title = this.refs.titleInput.getDOMNode().value;
var link = this.refs.linkInput.getDOMNode().value;
var bookmark = {title: title, link: link};
bookmarks.push(bookmark);
this.setState({bookmarks: bookmarks});
},
User story covered: When I click on the delete button of a bookmark, it removes the bookmark from the list and delete it in database.
You noticed that in the previous render function we gave a removeLine
function as parameter of each bookmark. It will allow to trigger the remove process from the bookmark component. But the removal will be handled properly from the bookmark list component.
removeLine: function(line) {
var bookmarks = this.state.bookmarks;
var index = 0;
while (index < bookmarks.length && bookmarks[index].link !== line.link) {
index++;
}
if (index < bookmarks.length) {
var bookmark = bookmarks.splice(index, 1)[0];
// Changement d'état.
this.setState({bookmarks: bookmarks});
}
},
Now let's go back to our bookmark component.
We add a listener to our button that will call a onDeleteClick
function of our component. This function will call the removeLine
function from the parent Component.
var Bookmark = React.createClass({
onDeleteClicked: function() {
bookmark = this.props;
this.props.removeLine(bookmark);
},
render: function() {
return (
<div>
<p class="title">{this.props.title}</p>
<p class="link">
<a href={this.props.link}>{this.props.link}</a>
</p>
<p>
<button onClick={this.onDeleteClicked}>X</button>
</p>
</div>
);
}
});
We ensure that the removeLine
function is well set on each bookmark component instances:
render: function() {
var bookmarks = this.state.bookmarks.map(function(bookmark) {
return (
<Bookmark title={bookmark.title} link={bookmark.link}
removeLine={removeLine}>
</Bookmark>
);
});
return (
...
<div>
<div>
{bookmarks}
</div>
</div>
);
}
Now, when you click on the delete button you will see the corresponding line disappear.
Go back to your browser and enjoy playing with your bookmark manager!
SuperAgent
Finally we need to be able to query the API that will be used to store the data. For that we'll use a query library called SuperAgent. It will allow us to make HTTP request from the browser.
First, we get the super agent dependency for doing request and slug dependency to build bookmark ID from the link.
var request = require('superagent');
var slug = require('slug');
Then, we create a module to simplify access to data:
var data = {
The first function will sends a HTTP request to the server to claim the bookmark list. We ask to the server to only send data at JSON format as a response.
Note that, at the end, we call a callback function given in parameter when the request is done.
getBookmarks: function(callback) {
request
.get('/api/bookmarks')
.set('Accept', 'application/json')
.end(function (err, res) {
callback(err, res.body);
});
},
The second function will send a POST request with bookmark info as body of the request. The data are formatted in the json format. It will allow to create bookmarks
on the server.
createBookmark: function(bookmark, callback) {
request
.post('/api/bookmarks')
.send(bookmark)
.end(function (err, res) {
callback(err, res.body);
});
},
To manage the bookmark deletion, we make a function that will send a DELETE request to the server. Here the id
of the bookmark is required. We build it by slugifying the bookmark link (we'll explain why in the next part of the tutorial).
deleteBookmark: function(bookmark, callback) {
request
.del('/api/bookmarks/' + slug(bookmark.link))
.end(callback);
}
}
Finally, we change our React code to now use our API request. After that our app will be broken until we write the code for the API.
var BookmarkList = React.createClass({
...
onAddClicked: function() {
...
// Here we modify the state.
this.setState({bookmarks: bookmarks});
// Here we save the change to the server.
data.createBookmark(bookmark, function () {});
},
removeLine: function(line) {
...
if (index < bookmarks.length) {
var bookmark = bookmarks.splice(index, 1)[0];
// Here we modify the state.
this.setState({ bookmarks: bookmarks });
// Here we save the change to the server.
data.deleteBookmark(bookmark, function () {});
}
},
...
});
// Before rendering the list we grab the data from the server.
data.getBookmarks(function(err, bookmarks) {
React.render(<App bookmarks={bookmarks}></App>,
document.getElementById('app'));
});
It changes two things:
- Addition and deletion action now sends a request to the server.
- Prior to rendering we grab the bookmark list to render.
Conclusion
You can see the final result on the github repository.
Because of the API requests, your app should not work anymore. In the third part of the tutorial we will explain how to write the server to put your bookmark app back on track!
We covered in this part the UI development. It's the most important because it contains the more complex logic. The server, in our case, will only handle persistence. It's going to be easier to implement.
Go read part 3: How to build your REST API with ExpressJS, Americano and PouchDB