This post is part of a series of mine on building web applications from the ground up. It covers everything you need to know to completely finish a web application. If you haven’t been following the series, please go back and read part 1.
Recap
In part 3 of this series, we made a RESTful API using Django REST Framework. In this part, I’ll be assuming that you have your API working and running at “localhost:8000”.
What’s a React!?
When I first started using React, I was as confused as a fish in a dishwasher. I vaguely understood React. I could make components on a page with plain old ES2015. But I wanted to use Babel and Webpack and Redux and all the other words. However, I had no idea how to actually set any of that up.
Luckily, I stumbled upon a few boilerplate projects that you can use to build your project. After looking them over, I found one that stood out: React Slingshot. It was easy to use, it setup your project with React, Babel, Webpack, and Redux (and more, if you want). But before I dive into project setup, let's review the terms I just mentioned.
React
React is “a JavaScript library for building user interfaces.” React isn’t a large framework that controls your entire application. It shouldn’t handle any business logic. It’s just for building nice web pages and components.
Babel
Babel is a JavaScript compiler. It is capable of taking newer versions of JavaScript that are not currently supported in the browser and compiling them into supported JavaScript versions. If you want to use ES6 features then you’ll want to use Babel or some alternative. We’re going to use babel in this tutorial though.
Webpack
Webpack is a module bundler. It takes all of the dependencies and source code your application requires including JavaScript, CSS, and HTML, and bundles them together to reduce the size of your application. For example, with Webpack you can compile all of your JavaScript into one file.
Create a New Git Repo
Before we get into the nitty-gritty stuff, follow the instructions I went over in part 2 of this series to make a new Git repository. Notice that because this app is a JavaScript app instead of a Python package, you may want to select the “Node” .gitignore file instead of the “Python” .gitignore file from the GitHub form. After you’ve created your new repository, clone it somewhere on your machine.
Initialize the React Project
Before you can setup your project, you’ll have to download Node.js. For this tutorial, you’ll need at least version 6.
Like I said before, I found it easiest when I was starting to use the React Slingshot project I found, and build my app off of that. So that’s what we’re going to do today.
Open up the folder of the git repository you made.
Follow the instructions found in the React Slingshot docs here.
After you have created the demo app, you can run it by running the following in your command line. This will start your development server.
npm run start
Play around with the demo app a little if you’d like. Once you’re done playing with it, do not run the “remove-demo” command as they suggest in the React Slingshot docs. As a beginner, I found it easier to modify the existing app, and then tear out all the crap I didn’t need.
Create a New Page
First, let’s create a new page. So, create a new file at src/containers/HomePage.js.
In this file, we’ll define a React component that we’ll use to represent a page of our website. This component will be composed of other components, and facilitate the use of state and actions throughout the components within the page.
There are essentially two types of components. The one we’re going to make in HomePage.js would be a “Container Component”. The other type is “Presentational Components”. Furthermore, Container Components are rightly named because they simply contain other components. They also map application state to props for the child components. Presentational components should be for presentation only, and should not need to interact directly with the application state or actions. Moreover, a presentational component should be able to properly function only using the variables it is given in its Props. For an even more in depth explanation of this, I’d recommend reading Dan Abramov’s post on the subject.
Anyways, we’re going to make a container component for our home page.
First, open the HomePage.js file we created.
In this file, add the following code.
import React from 'react'; export default HomePage = (props) => { return ( <div>Hello</div> ); };
Right now, this is a fairly simple component. Once we setup our routes, it will merely show the word “Hello” rendered on the screen.
Now, open the routes.js file. Add the following import statement at the top:
import HomePage from './components/HomePage';
Then, change the index route to the following:
<IndexRoute component={HomePage}/>
You will also have to open components/App.js, and change the following import statement:
//import <HomePage from './HomePage'; //change this import HomePage from '../containers/HomePage'; //to this
Now, if you run the npm run start command, you should see the “Hello” text we entered in our HomePage component.
At this point, we can start ripping out the rest of the files we are no longer using. I would at least recommend ripping out all of the Fuel Savings demo files.
The following can be deleted:
- FuelSavingsReducer.js
- FuelSavingsReducer.spec.js
- FuelSavingsPage.js
- FuelSavingsPage.spec.js
- fuelSavingsActions.js
- fuelSavingsActions.spec.js
- AboutPage.js
- AboutPage.spec.js
- And all FuelSavings* files in the components/ folder.
After deleting these, you’ll also have to remove any references to them in the following files:
- App.js
- routes.js
- reducers/index.js
Double-check for any additional references I might have missed and run your app again. This should show us the same old “Hello” message we made before.
Using state in a container
This is great progress, however, we’re going to need to utilize state in our container.
First, let’s edit our initialState.js file, which is in the reducers folder. Here, we can define the state that our application starts with before it makes any requests to any APIs.
For the time being, we can define some dummy data here to use as we develop our components. Change the initialState to look as follows:
export default { todoLists: [ { "id": 1, "title": "Test Todo List #1", "items": [ { "id": 1, "todo_list_id": 1, "title": "Item 1", "description": "This is item 1." }, { "id": 2, "todo_list_id": 1, "title": "Item 2", "description": "This is item 2." } ] }, { "id": 2, "title": "Test Todo List #2", "items": [ { "id": 1, "todo_list_id": 1, "title": "Step 1", "description": "This is the only item." } ] } ] };
Before we can use this data in our HomePage container, we’ll have to write a reducer to handle the initializing of state and the changing of state via actions.
Create a Reducer
Create a new file at reducers/todoListReducer.js
In this file, add the following lines, which initialize our state with the values we defined above.
import objectAssign from 'object-assign'; import initialState from './initialState'; /* the reducer is invoked when actions are executed and on initialization. */ export default function todoListReducer(state = initialState.todoLists, action) { let newState; /* write different cases for different actions, and fall back to the initial state */ switch (action.type) { default: return state; } }
Now, we need to add this reducer to the root reducer. To do this, open reducers/index.js.
First, add the following import statement to the top of the file:
import todoLists from './todoListReducer';
Then, modify the combineReducers function call to look like so:
const rootReducer = combineReducers({ todoLists, routing: routerReducer });
Now our initialState will load into our application state on startup. Later, this reducer will be handy for applying state changes when actions are invoked.
Mapping State
Now we can pass this state into our HomePage container. In the HomePage.js file, add the following imports to the top of the file:
import PropTypes from 'prop-types'; import {connect} from 'react-redux';
Now, we’ll define the props our HomePage is going to receive. Props in React are attributes that get passed into the component. By defining our component’s PropTypes, we can enforce the types of props that get passed in.
Add the following function at the bottom of the file, which will specify the proptypes our HomePage accepts.
HomePage.propTypes = { todoLists: PropTypes.array.isRequired };
Since our component is going to receive the todoLists prop from the application state, we need to map the state to the props. The following code placed at the end of your file will accomplish just that.
function mapStateToProps(state) { return { todoLists: state.todoLists }; }
export default connect( mapStateToProps, )(HomePage);
However, currently our HomePage variable is the default export for this file. In JavaScript, there can only be one default export. For those of you who don’t know what a default export is, it essentially says that if something is going to be imported from this file, let it be this default export.
So, in our HomePage container where it says “export default HomePage”, change that to “const HomePage”. This will make it so the output of the “connect” function is exported from this file instead, which has the state mapped to the props properly.
Just to make sure this is working, add a log statement to your container like so:
const HomePage = (props) => { console.log(props.todoLists); return ( <div>Hello</div> ); };
Run the app and look in your browser’s content. You should see the initial state we defined being output. And now we’ve successfully started utilizing our application’s state in the HomePage.
Create a ToDo List Component
Make a new file named components/ToDoList.js. In this file, we’ll make a component to handle the rendering of a single todo list. This means that this component should display the ToDo list title, as well as each of its items. Add the following to your ToDoList file.
/* eslint-disable import/no-named-as-default */ import React from 'react'; import PropTypes from 'prop-types'; class ToDoList extends React.Component { render() { // first, create some HTML for each todo list item const todoListItems = this.props.todoList.items.map((todoListItem) => { // items in lists need unique keys in react. So lets make one from our ids const uniqueKey = todoListItem.todo_list_id + '-' + todoListItem.id; return ( <li key={uniqueKey}> <strong>{this.props.todoListItem.title}: </strong> <span>{this.props.todoListItem.description} </span> <button>Delete</button> </li> ); }); // Then, include the todo list items in the HTML for this todo list. return ( <div> <h1>{this.props.todoList.title} <button>Delete</button></h1> <ul> {todoListItems} </ul> </div> ); } } ToDoList.propTypes = { todoList: PropTypes.object.isRequired }; export default ToDoList;
This component requires a todoList prop when it is used. Using this prop object, it can extract the ToDoList title, as well as each of its items. If you inspect the code, you’ll see that the code to render the items is constructed first, and then included within the return statement of the render function. This is a typical way to handle the construction of items in an array.
Another thing to note here is that we have delete buttons strategically placed next to all of our items and lists. We’ll wire these up to actions later in order to implement this functionality.
In order to see this in action, add the following import statement to your HomePage.js file:
import ToDoList from '../components/ToDoList';
Then, make your home page render each todo list:
const HomePage = (props) => { // first create HTML for each todo list const todoLists = props.todoLists.map((todoList) => { return <ToDoList key={todoList.id} todoList={todoList} />; }); return ( <div> <h1>My Todo Lists</h1> {todoLists} </div> ); };
In this code, we create the code to render each todo list, then add it in our return statement right below the “My Todo Lists” title.
If you run your app now, you should see the UI we made show up.
Make a form for adding lists and items
Now we just need to stub out some forms for creating Todo lists and items. Let's keep it simple, and stick with pretty basic HTML for this one. Add the following HTML to your HomePage render function:
... // define the options to include in the form const todoListOptions = props.todoLists.map((list) => { return <option key={list.id} value={list.id}>{list.title}</option> }); return ( <div> <h1>My Todo Lists</h1> {todoLists} <h1>Add A Todo List</h1> <input name="title" type="text" /> <button>Add</button> <h1>Add A Todo List Item</h1> <label>ToDo List: </label> <select name="list_id"> {todoListOptions} </select> <br/> <label>Title: </label> <input name="item_title" type="text"/> <br/> <label>Description: </label> <input name="item_desc" type="text"/> <br/> <button>Add</button> </div> );
This code adds two headings for the Todo list and Todo list item inputs. In the Todo List section, we have a basic input where users can enter a title. Then, we have an add button which we implement the functionality for later.
In the Todo list item section, our inputs are a bit more complex. Here all of our inputs have labels that help describe the input to the user. We have a select box so the user can select which list they would like to add an item to. Note that we made the list of options at the very top of this code excerpt. Next, we have a label and input for the title of the item, followed by the same for the description of the item.
Creating Actions
In React with Redux, actions are functions that perform state changes and API requests. These are observed by the reducers, which apple the changes to the state.
Right now, we’re going to create 3 actions:
- fetchTodoLists
- addTodoList
- addToDoListItem
Create a new actions/todoListActions.js
Before we start making actions, let’s open constants/actionTypes.js and add the following constant. These will define the various actions that our reducers will observe and that our actions will broadcast. We will have more actions than what is in this file, but this will contain the observable actions.
export const RECEIVE_TODOS = 'RECEIVE_TODOS';
Now, open our actions file and let’s start codin’.
Import the constants we just defined.
import * from '../constants/actionTypes';
Now, we’ll define a function which handles the receiving of our todo lists after an API request is made.
function receiveTodos(json) { return function(dispatch) { return dispatch({ type: RECEIVE_TODOS, todos: json }); }; }
This function dispatches an action with the data it gets passed in. This action will be observed by a reducer, which will apply the data to the state. The function will be called in our API request after it has completed.
Now, we need to write a function to fetch the data. However, if we want to make API requests, we’ll need to install the fetch library:
npm install isomorphic-fetch --save
Now, add the following import to your actions/todoListActions.js file.
import fetch from 'isomorphic-fetch';
Now, we can write a function for fetching our todo lists.
export function fetchTodos() { return function(dispatch) { return fetch('http://localhost:8000/todo_lists') .then(response => response.json()) .then(json => dispatch(receiveTodos(json))); } }
This function makes a request to our API at “http://localhost:8000/todo_lists”, then takes the results and uses them as a parameter when calling receiveTodos(). Notice that we are exporting this function. This means that we are making this publicly available to any files importing this one.
Next, lets write an action to add a new todo list. To do this, we’ll have to make a POST request.
export function addTodoList(title) { return function(dispatch) { // make the POST request with our data return fetch('http://localhost:8000/todo_lists', { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, method: 'POST', body: JSON.stringify({ title: title, items: [] }) }) // after the request finishes, update the todos .then(() => { return dispatch(fetchTodos()); }); }; }
This uses the expanded form of fetch where we can define the method and body explicitly. In this function, we make a POST request with our title as the only data. Then, we dispatch a call to run our fetchTodos action again to update our todo list.
One last thing we’ll need to add is an action to add todo list items. Let’s write that quickly below. It will be much the same as the addTodoList action, except with more data.
export function addTodoListItem(todoListId, title, description) { return function(dispatch) { // make the POST request with our data return fetch('http://localhost:8000/todo_lists/'+todoListId+'/items', { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, method: 'POST', body: JSON.stringify({ title: title, description: description }) }) // after the request finishes, update the todos .then(() => { return dispatch(fetchTodos()); }); }; }
BAM. We now have actions that we can call in our components to make API requests. But what will happen when those requests come back? Right now, nothing. However, we’ve designed these in such a way that a reducer can listen for RECEIVE_TODOS actions, and update the state with the results from the API. So now lets add some logic to our reducer to do this.
Add the following to your todoListReducer:
export default function todoListReducer(state = initialState.todoLists, action) { let newState; /* write different cases for different actions, and fall back to the initial state */ switch (action.type) { case RECEIVE_TODOS: // always ALWAYS copy your state before modifying it // in this case, we're assigning the entire state to a new value, so it would be pointless //copy state if modifying it: newState = objectAssign({}, state); newState = action.todos; break; default: return state; } }
This will save the values received from the API into our state’s todoLists section.
Invoking Actions from Components
Now we’re ready to invoke these actions from our components.
First, let’s change our HomePage.js component into a class-based component. This will make it easier to initialize and handle state in our container.
To do this, we’re going to take everything being done in our homepage function, and move it into the render() function of a new class. The result, should look like this:
class HomePage extends React.Component { render() { //OLD CODE HERE } }
Before we can call our actions, we’ll have to import them and bind them to our container. To do this, first import the from the file we created above. You’ll also have to import the bindActionCreators function.
import {bindActionCreators} from 'redux'; import * as actions from '../actions/todoListActions';
Then, we’re going to have to modify everything below where you define your proptypes. The end of the file should now look like this:
// add "actions" as a proptype your container accepts. HomePage.propTypes = { todoLists: PropTypes.array.isRequired, actions: PropTypes.object.isRequired, }; // this block doesn't change since we're not adding more state function mapStateToProps(state) { return { todoLists: state.todoLists }; } // this function maps actions to the prop we defined above. function mapDispatchToProps(dispatch) { return { actions: bindActionCreators(actions, dispatch) }; } // and the connect function applies the mapDispatchToProps function on an instance of our container. export default connect( mapStateToProps, mapDispatchToProps )(HomePage);
Now, we’re going to add another function to this component called componentDidMount(). In this function, we can make our initial action call to get our Todos.
componentDidMount() { this.props.actions.fetchTodos(); }
Now, if you open your browser, you should see that your action is being called, and the data from your API is now being used instead of the fake data defined in initialState.js (which you may now remove).
But wait, I’m getting a “No ‘Access-Control-Allow-Origin’ header” error in my console.
This is because the browser does not let you make requests from your domain to a different domain by default. For local development, you can allow this through a number of mechanisms. To do this throughout all my projects, I usually use a CORS chrome extension.
Getting Form Input
Now that we have one action working, let's finish the rest. Earlier, we made a simple form with plain-Jane HTML. Let’s add handler functions to this to save the values from these inputs when they change. Then, we can add logic to submit the form later.
First, we’ll have to add onChange functions to each of our inputs. Let's add this function now, and we can define it later.
The insides of your form should look like this after adding the onChange functions:
<h1>Add A Todo List</h1> <input name="list_title" type="text" onChange={this.handleChange}/> <button>Add</button>
<h1>Add A Todo List Item</h1> <label>ToDo List: </label> <select name="list_id" onChange={this.handleChange}> {todoListOptions} </select> <br/> <label>Title: </label> <input name="item_title" type="text" onChange={this.handleChange}/> <br/> <label>Description: </label> <input name="item_desc" type="text" onChange={this.handleChange}/> <br/> <button>Add</button>
Now, let’s add the onChange function we used. Note that the code below will require you to import the objectAssign package, which you should be able to do at this point in time.
handleChange(event) { let newState = objectAssign({}, this.state); newState[event.target.name] = event.target.value; this.setState(newState); }
When an HTML input is changed, the browser will execute this handler with an event object which contains information about the change. When this function is called, we can save the input into our component’s local state, using it’s name attribute as the identifier. But wait, our component doesn’t have local state? We can give our component local state to manage by defining this.state in the constructor. We’ll also have to add to the constructor some code to bind the handleEvent function we defined to our class’s scope. So let’s add the following constructor to our class:
class HomePage extends React.Component { constructor(props) { // initializes the base class super(props); this.state = { // used in the add todo list form list_title: "", // used in the add todo list item form list_id: "", item_title: "", item_desc: "", }; // binds the handleChange function to this instance of 'this' this.handleChange = this.handleChange.bind(this); } ...
Now, when we enter text in our inputs, it will be saved to the local container state.
Bringing it All Together
Now that we have form input being saved, we can implement our add todo list and add todo list item features.
We’ll have to define the following functions in our class to read the input from the form and make the proper action calls. Remember, that you’ll have to add lines to your constructor to bind each function to the instance of ‘this’.
/* Adds a todo list using the todo list title input. */ addList() { // if the input isn't empty if(this.state.list_title !== "") { // make the call to our action to add the list this.props.actions.addTodoList(this.state.list_title) // after that call completes, do the following .then(() => { // reload the todo lists from the API this.props.actions.fetchTodos(); }); } } /* Adds a todo list using the todo list title input. */ addItem() { // if the required inputs are filled out if(this.state.list_id && this.state.item_title !== "") { // call the action with all our input data this.props.actions.addTodoListItem( this.state.list_id, this.state.item_title, this.state.item_desc ) // then re-fetch the todo list data from the API .then(() => { this.props.actions.fetchTodos(); }); } }
After these have been bound properly in the constructor, we can add them to our buttons to be called when the user clicks them. Do this by adding the onClick method as follows:
<button onClick={this.addList}>Add</button> <button onClick={this.addItem}>Add</button>
This will trigger each function when the button is clicked. The first button adds a list, while the second button adds a list item.
Now, if you run the app you should be able to add lists and items and see the results automatically update in your browser.
Conclusion
If you’re itching for more, consider going back and implementing the stubbed out functionality to delete todo lists. I left these buttons there purposely, in order to annoy you into going above and beyond. If you’re like me, the thought of such a tasty piece of functionality merely being stubbed out when it could be finished now will drive you crazy.
We covered a lot in this tutorial, so if I have lost you at any point, please ask a question below.
There are many more topics to be covered in this series such as configuration management, deployment, and user authentication, so Subscribe to get the latest updates!