Create a React Front End with React Router
By Steve Carey - Last Updated 7/3/2019
Finished code for this section combined with the Rails API: Github
This is part of a series of tutorials on APIs and the React library. See the full list at Tutorials. Throughout this series we build a Create-Read-Update-Delete (CRUD) application with a React front end and a Ruby on Rails back end. Each section is self-contained.
The first tutorial in this series showed you how to build a Rails Application Programming Interface (API). This section will show you how to build the matching React client as a separate application. We will be interacting with the Rails API we created, but if you prefer to make the API with another framework it would also apply. The back end needs a database table named "articles" with fields for id, title, and content, and a server-side application to provide the API endpoints.
React is a JavaScript library for building user interfaces. It represents the View part of the Model-View-Controller architecture that Rails and probably most web application frameworks use. You need a separate back-end API with endpoints to interact with to get, post, update or delete data.
This tutorial assumes you already have some familiarity with React. If you are brand new to React it is possible to do this tutorial while simulaneously learning React. The React.js website has good information including a Getting Started guide, an introductory tutorial, and a Main Concepts section. You should be familiar with the concepts of JSX, render, components, props, state, lifecycle, events, lists/keys, and forms. Going through this tutorial should give you a solid grasp on these concepts. We will be using the React Router package for routing so it helps if you have some familiarity with it, but that's also something you can learn as you go. You'll certainly be seeing it in action.
React is all in with ES6. It uses the Babel compiler to transpile ES6 to ES5 so you don't need to worry about end user browser support. This tutorial assumes you are familiar with ES6 syntax for classes, arrow functions, let and const variables, template strings using back ticks (``), module imports, and promises. We will be using all of those.
Beginning with Rails 5.0 you have the option of generating an API only application instead of the traditional Rails app. A Rails API-only app is a slimmed down version of a regular Rails web app, skipping the Asset Pipeline and generation of view and helper files. You can read the details in the Rails Guides - API App.
mkdir appname; cd appname
To generate an API-only app just add the --api flag
rails new . --api
rails generate scaffold Article title:string content:text
rails db:migrate
# db/seed.rb articles = Article.create([ {title: "Learn Ruby", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, {title: "Learn Rails", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}, {title: "Learn React", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."} ])
rails db:seed
Add the rack-cors gem to prevent Rails from blocking cross browser access, since our front end and back end will be running on different ports.
# Gemfile gem 'rack-cors'
bundle install
# config/application.rb module ArticlesAppWithApi class Application < Rails::Application ... config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:3000' resource '*', :headers => :any, :methods => [:get, :post, :put, :patch, :delete, :options] end end end end
Add an "api" namespace to the routes. This will avoid potential route collisions between your back-end api and your front-end react routes.
# config/routes.rb scope '/api' do resources :articles end
rails server -p 3001
Done!
You need to have Node and Yarn installed on your computer to use React. We will install and use create-react-app to get us going.
Create-react-app is a Node package developed by Facebook that configures your React app for you and gives you a starter app out of the box. It includes an already configured Webpack server that bundles all the JavaScript files in our app into a bundle.js file that gets attached into the index.html file at runtime. At runtime, Webpack’s dev server will listen for you to save changes in develepment, and will automatically load your running web page with the changes. This is called hot reloading. Install it as a Node global package:
npm install -g create-react-app
We already created a Rails application that we will be using as our back end. We could keep the front end and back end code in completely separate locations, but if we are to deploy our application in production as an integrated web application it's easier to keep them together. We'll add a directory called client to our Rails project and place our React application there.
create-react-app client
cd client
Now believe it or not we actually have a working React app ready to go. Start the server:
yarn start
We will be using two third party packages, React Router for routing and Axios for AJAX requests.
Yarn and npm (Node Package Manager) are both package managers for managing JavaScript dependencies. You can use either, but since React and Yarn were both created by Facebook, they tend to interact tightly together so we will be using Yarn to install packages (yarn install package-name
) and run the server (yarn start
).
If you don't want to cd into the client folder you can run the yarn commands from the project root directory by inserting the current working directory (cwd) option like this: yarn --cwd client start
.
Inside our React file structure is package.json. This file lists the locally installed packages as dependencies. You should see react, react-dom, and react-scripts in there. This file is loosely akin to the Rails Gemfile. You can read about it at docs.npmjs.com - package.json.
Install the react-router-dom and axios packages:
yarn add react-router-dom axios
Now if you look back at the packages.json file you should see those packages as dependencies.
If you look at the files and directories generated in the client directory, besides a folder for the node_modules you'll see a "public" and a "src" directory. These are where you add your own files. Right now there are no sub-directories in those folders so things can get pretty cluttered quickly if we don't add some structure. From the client directory use these UNIX commands to add new folders, move files in them, and add the files we'll need for our React app. Or do it manually if you prefer.
mkdir public/images mv public/favicon.ico public/images/favicon.ico mkdir src/images mkdir src/stylesheets mkdir src/components mv src/logo.svg src/images/logo.svg mv src/index.css src/stylesheets/index.css mv src/App.css src/stylesheets/App.css mv src/App.js src/components/App.jsx mv src/App.test.js src/components/App.test.js touch src/components/Home.jsx touch src/components/ArticleList.jsx touch src/components/ArticleInfo.jsx touch src/components/ArticleAdd.jsx touch src/components/ArticleEdit.jsx
In the public directory we created a folder for images then moved the favicon image there. In the src directory we created folders for our components, stylesheets, and images and moved the relevant files there. Much better, except now our React app is broken. Fix the broken links and imports to get it working again:
# public/index.html <link rel="shortcut icon" href="%PUBLIC_URL%/images/favicon.ico">
# src/index.js import './stylesheets/index.css'; import App from './components/App';
# src/components/App.jsx import logo from '../images/logo.svg'; import '../stylesheets/App.css';
Save each file after you make the changes, then go check the browser. The app should be working again.
The index.css file is for our app-wide css. The App.css file is specific to the App.component we are going to fill out. Delete the CSS that's in there now so it doesn't conflict with our app. If you want to add custom CSS classes this is where you would do it.
# src/stylesheets/App.css /* Remove all the CSS classes */
For convenience we'll use the Bootstrap CDN to style our app. You can either put the CDN link in the head element of the index.html file:
# client/public/index.html <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css" integrity="sha384-Smlep5jCw/wG7hdkwQ/Z5nLIefveQRIY9nfy6xoR1uRYBtpZgI6339F5dgvm/e9B" crossorigin="anonymous">To use the JavaScript features you need to load jQuery, popper, and Bootstrap. Just before the closing body tag add:
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous">Or alternatively put it in the index.css stylesheet. Of course, feel free use your own styles if your prefer.
# client/src/stylesheets/index.css @import url('https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css');
This is a Single-Page Application (SPA) with CRUD capabilities. It is indeed single page. We only have one html file sitting in the public folder: public/index.html. It has your standard HTML page structure with one element inside the body. An empty div tag with id name of root <div id="root"></div>
. That is where React renders it's output with JavaScript. There's nothing special about the name "root." It can be any name.
Then in the src/index.js file we import the React library and render our main component. Your src/index.js file should look like this.
import React from 'react'; #1a import ReactDOM from 'react-dom'; #1b import './stylesheets/index.css'; import App from './components/App'; #1c import * as serviceWorker from './serviceWorker'; ReactDOM.render(<App />, document.getElementById('root')); #2 serviceWorker.unregister(); #3
Let's talk about the files we created in the components directory. With React you split your user interface into components. Each component returns some JSX which ultimately gets rendered as HTML to the user interface. In our case we are creating the front end to a CRUD application. In a Rails application we would have our layout file that would contain our navigation bar and yield our views. Then for our Articles resource we would have views for index, show, new, and edit.
For our React app, a logical split for our components is to mirror the Rails resource structure. Our App.jsx file is like the Rails app/views/layouts/application.html.erb file. It holds the structure for our web page. The ArticleList, ArticleInfo, ArticleAdd, and ArticleEdit files correspond to the Rails index, show, new, and edit view files respectively. We'll also add a Home component to be our home page. Note that component names must be capitalized. Each file will hold a separate React component, with the exception of App.jsx which will hold three. We could break it out even further, like a form component that gets imported to the ArticleAdd and ArticleEdit components, but we won't.
Let's start out with a very simple component called Home. We already created an empty Home.jsx file. Populate it with the below:
# src/components/Home.jsx import React from 'react'; 1 const Home = () => { 2 return ( <div className="jumbotron"> <h1>Home Page</h1> </div> ); } export default Home; 3
Right now this file is just sitting in space not connected to anything. If you want to see it in action you can temporarily open the src/index.js file and change the App elements to Home. Then go to your browser and it should now say "Home Page."
# src/index.js ... import Home from './components/Home'; ... ReactDOM.render(<Home />, document.getElementById('root'));
Change the index.js file back the way it was. Referencing "App."
The term Single-Page App can be a little misleading. While it is indeed a single html page, that doesn't mean you can't have multiple views with changes to the URL. We will use the React Router package that we installed to do just that. And we will also perform the standard CRUD actions in our single page interacting with the API to get articles, post new articles, edit existing articles, and delete articles. Instead of doing those things all on separate HTML pages as you would in an traditional web app, we will do it from our single page using AJAX calls with the help of the Axios package we installed. React will move the data around using JavaScript which can add significant speed over entire page loads from the server.
You may have noticed we changed the extension of the App file from .js to .jsx. Either will work since React converts .jsx files into .js files when it is compiled. But since that file does contain JSX in it, it is more correct to give it the .jsx extension in my opinion. Also I installed the Babel package in my (Sublime Text) text editor which gives me automatic JSX syntax highlighting for files with the .jsx extension.
The central component for our app is the App component. That is what is returning the Welcome to React page we see in the browser. Replace that with a Navigation bar and a place to render the other components. Here is the code for the App.jsx file all at once:
# src/components/App.jsx
import React, { Component } from 'react';
import {BrowserRouter as Router, Route, NavLink, Switch} from 'react-router-dom';
import '../stylesheets/App.css';
import Home from './Home';
import ArticleList from './ArticleList';
import ArticleInfo from './ArticleInfo';
import ArticleAdd from './ArticleAdd';
import ArticleEdit from './ArticleEdit';
class App extends Component {
render() {
return (
<div className="App">
<Router>
<div className="container">
<Navigation />
<Main />
</div>
</Router>
</div>
);
}
}
const Navigation = () => (
<nav className="navbar navbar-expand-lg navbar-dark bg-dark">
<ul className="navbar-nav mr-auto">
<li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li>
<li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li>
</ul>
</nav>
);
const Main = () => (
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/articles" component={ArticleList} />
<Route exact path="/articles/new" component={ArticleAdd} />
<Route exact path="/articles/:id" component={ArticleInfo} />
<Route exact path="/articles/:id/edit" component={ArticleEdit} />
</Switch>
);
export default App;
First thing's first. Once you add the code and save the file, go to the browser and make sure you didn't get any errors. We removed the Welcome to React header and logo and replaced it with a navigation bar with two links, and a simple home page jumbotron. If you click on the articles link you'll get a blank page because we haven't populated that yet.
This page is all about Navigation and the React Router package. React Router is the most popular Routing package. The docs are at reacttraining.com/react-router/web/guides which looks like a third party training site, and it is. But they are the ones who created the React Router package (not Facebook's React team).
Alright, let's break this down.
At the top are all of our imports:
import React, { Component } from 'react'; #1 import {BrowserRouter as Router, Route, NavLink, Switch} from 'react-router-dom'; #2 import '../stylesheets/App.css'; import Home from './Home'; #3 import ArticleList from './ArticleList'; import ArticleInfo from './ArticleInfo'; import ArticleAdd from './ArticleAdd'; import ArticleEdit from './ArticleEdit';
1) To create a component we need to import the React library.
2) Import the components you need from the react-router-dom module. React Router also has a react-router-native module for mobile apps. In this case we'll import the BrowserRouter (giving it an alias of Router), Route, NavLink, and Switch components.
3) Import the other components that we will be calling with our Routes. Each of those files will contain a class or functional component. We could just put those in the App file then we wouldn't have to import them. And indeed we did do that for the Navigation and Main components.
The App class component's render method:
class App extends Component { render() { return ( <div className="App"> <Router> #1 <div className="container"> <Navigation /> #2 <Main /> #3 </div> </Router> </div> ); } }
1) In the App class render method we appended the <Router> element to manage our routing. It contains two custom elements:
2 & 3) <Navigation /> and <Main /> are custom elements that call the corresponding components. Nothing special about those names, you could call them what you like. Those components return JSX that gets inserted into the App component.
The Navigation component:
const Navigation = () => ( #1 <nav className="navbar navbar-expand-lg navbar-dark bg-dark"> <ul className="navbar-nav mr-auto"> <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink></li> #2 <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li> #3 </ul> </nav> );
1) The Navigation component returns JSX that ultimately renders the nav bar using the Bootstrap classes we provide here.
2&3) The React Router NavLink component is a subset of a Link component that we'll use shortly. It provides the activeClassName property to style the link differently when it's active. The other thing to note is the "exact" attribute, which is the shorthand for "exact=true." That means the route has to be the exact route provided with the "to" attribute. The default is that it just contains the route provided. So the "/" route by default would include any route that contains "/", which is all routes. So we need the exact attibute here.
The Main component:
const Main = () => ( #1 <Switch> #2 <Route exact path="/" component={Home} /> #3 <Route exact path="/articles" component={ArticleList} /> <Route exact path="/articles/new" component={ArticleAdd} /> <Route exact path="/articles/:id" component={ArticleInfo} /> <Route exact path="/articles/:id/edit" component={ArticleEdit} /> </Switch> );
1) The Main component is where we insert all our Route elements.
2) The React Router switch statement works like a JavaScript switch statement. It checks each statement below it in order until there is a match.
3) Route is a React Router element that takes as attributes the path and the component to call if there is a match. The "exact" attribute requires the match be exact.
Now it's time to do our first API call. We'll go to the server to retrieve a list of all the articles in the database. In our file structure setup we created the ArticleList.jsx file which will be our React equivalent to the Rails index.html.erb file. Populate it with the ArticleList component below and save it.
# src/components/ArticleList.jsx
import React, { Component } from 'react';
import { get } from 'axios';
import { Link } from 'react-router-dom';
class ArticleList extends Component {
constructor() {
super();
this.state = { articles: [] };
}
componentDidMount() {
get('http://localhost:3001/api/articles.json')
.then(response => {
this.setState({articles: response.data});
})
.catch(error => console.log('error', error));
}
render() {
return (
<div>
{this.state.articles.map((article) => {
return(
<div key={article.id}>
<h2><Link to={`/articles/${article.id}`}>{article.title}</Link></h2>
{article.content}
<hr/>
</div>
)
})}
<Link to="/articles/new" className="btn btn-outline-primary">Create Article</Link>
</div>
)
}
}
export default ArticleList;
Let's see if it works. Since we will be calling the Rails API application we created in Part 1, make sure the server is running on port 3001.
rails server -p 3001
Now if you go to the browser and click on the Articles link you should see a list of articles. This corresponds with the JSON view at http://localhost:3001/api/articles.json which you can view in a separate browser tab. The connection to the API is working! Also, notice that we are running two separate servers on two different ports. One for the Rails back-end app and one for the React front-end app.
Time to break it down starting with...
The imports:
import React, { Component } from 'react'; #1 import { get } from 'axios'; #2 import { Link } from 'react-router-dom'; #3
1) Import "Component" from the React library.
2) We need to make an AJAX call using the HTTP GET method so we need to import that from the Axios package we added earlier.
3) We will include links to the the individual Article pages and to the New Article form, so import the Link component from the react-router-dom module.
class ArticleList extends Component { #1 constructor() { #2 super(); this.state = { articles: [] }; #3 }
1) We create a class component called ArticleList.
2) At the top it needs a constructor function.
3) In it we set the initial state to an empty articles array:
ComponentDidMount:
componentDidMount() { #1 get('http://localhost:3001/api/articles.json') #2 .then(response => { #3 this.setState({articles: response.data}); #4 }) .catch(error => console.log('error', error)); #5 }
1) Then we use the built-in React componentDidMount() method. This is called a lifecycle method since it changes the state of the component.
2) It contains our AJAX call to the API.
3) and uses ES6 promises to wait for the response.
4) Once received it uses the React setState method to assign the response data object to the empty articles array we declared in the constructor.
5) Add a catch method to log any errors to the console.
Render:
render() { #1 return ( <div> {this.state.articles.map((article) => { #2 return( <div key={article.id}> #3 <h2><Link to={`/articles/${article.id}`}>{article.title}</Link></h2> #4 {article.content} <hr/> </div> ) })} <Link to="/articles/new" className="btn btn-outline-primary">Create Article</Link> #5 </div> ) }
1) The render method returns JSX listing each article
2) This.state.articles is an array of the article objects we got from the API. The JavaScript map method iterates though the array transforming each item based on the function provided and returning a new transformed array.
3) React requires that we assign a unique key to each item when iterating though a list, so we are assigning the article id attribute.
4) Here we transform the article objects into JSX that puts the article title into a link, displays the article content, and adds a horizontal line at the bottom.
5) At the end we include a link to the new articles route.
If you want to get a better understanding of state then make use of logging. Add a console.log("this.state", this.state);
statement in the constructor before setting this.state to the empty articles array. Then add it to the componentDidMount() method before and after setting state to the response.data. Then go to the web page and open Chrome Developer tools (or the Firefox equivalent) and view the console tab. You'll see the lifecycle of this state. First it is undefined. Then it's an object with an empty articles property. Then, it's an object with an articles array containing objects for each article returned.
Another useful tool is the React Developer Tools plugin for Chrome or Firefox. If you Google it you'll see the link and can install it directly to your browser. Then when you open Developer Tools on your web page there will be a new React tab to the right. Click on it and you can either drill down to the ArticleList component or use the search box to find the ArticleList component right away. Then to the right you can see the props and state objects.
On to the ArticleInfo component which would correspond in Rails with an apps/views/articles/show.html.erb file. Populate the file with the below and save it.
import React, { Component } from 'react'; import axios from 'axios'; import { Link } from 'react-router-dom'; class ArticleInfo extends Component { constructor() { super(); this.state = { article: {} }; this.handleDelete = this.handleDelete.bind(this); } componentDidMount() { axios.get(`http://localhost:3001/api/articles/${this.props.match.params.id}.json`) .then((response) => { this.setState({ article: response.data }) }) .catch(error => console.log('error', error)); } handleDelete() { axios.delete(`http://localhost:3001/api/articles/${this.props.match.params.id}.json`) .then(() => { this.props.history.push("/articles") }) .catch(error => console.log('error', error)); } render() { return ( <div> <h2>{this.state.article.id}: {this.state.article.title}</h2> <p>{this.state.article.content}</p> <p> <Link to={`/articles/${this.state.article.id}/edit`} className="btn btn-outline-dark">Edit</Link> <button onClick={this.handleDelete} className="btn btn-outline-dark">Delete</button> <Link to="/articles" className="btn btn-outline-dark">Close</Link> </p> <hr/> </div> ) } } export default ArticleInfo;
Now test it out in the browser. From the Articles page, if you click on an article title it should take you to the article page. If you click on the second article (with id 2), it will call the API endpoint which you can also view directly at http://localhost:3001/api/articles/2.json.
The Imports:
import React, { Component } from 'react'; import axios from 'axios'; #1 import { Link } from 'react-router-dom';
The imports are the same as for ArticleList.
1) The only thing to note is we are importing the whole Axios library rather than just the specific methods we are using (get and delete). That's because "delete" is a JavaScript reserved word. Since we're import the whole Axios library, we call the specific methods with axios.get and axios.delete.
The constructor:
constructor() { super(); this.state = { article: {} }; #1 this.handleDelete = this.handleDelete.bind(this); #2 }
1) The constructor is similar to ArticleList except we set state to an empty article object rather than an empty articles array.
2) Also in this component we are including a delete button which will send an HTTP request to delete the article. You have to bind event handler functions like handleDelete in the constructor. If you don't it won't work. The explanation as why you need to bind is kind of involved so we won't go into detail. But in short, JavaScript will change what "this" means when the (onClick) event calls a separate handler function. From the object (like article id: 2) to undefined. Binding "this" to the this.handleDelete() function in the constructor will prevent that change.
ComponentDidMount:
componentDidMount() { #1 axios.get(`http://localhost:3001/api/articles/${this.props.match.params.id}.json`) #2 .then((response) => { #3 this.setState({ article: response.data }) }) .catch(error => console.log('error', error)); #4 }
1) The componentDidMount() method is similar to the same method in ArticleList.
2) But we do need to send the article id with our GET request to the API. So now the props object comes into play. If you look in the Chrome Developer Tools "React" tab and search on ArticleList you'll see the props and state objects in the pane to the right. Props contains three objects, one of which is called Match. Match contains the path (articles/:id), the url (articles/2), and another object called params. Params contains a single path param of :id. So to get the article id we need to chain this all together with this.props.match.params.id
.
3) When the response to the AJAX request comes back we will use the setState method to assign the empty article object to the response data for article 2 (or whatever article we clicked on).
4) If you manually enter an id in the URL that doesn't exist then the catch method takes over. We'll log the error message to the console. In this case it will log a 404 status code, meaning the record wasn't found.
Let's jump down to the render method next.
Render:
render() { return ( #1 <div> <h2>{this.state.article.id}: {this.state.article.title}</h2> <p>{this.state.article.content}</p> <p> <Link to={`/articles/${this.state.article.id}/edit`} className="btn btn-outline-dark">Edit</Link> #2 <button onClick={this.handleDelete} className="btn btn-outline-dark">Delete</button> #3 <Link to="/articles" className="btn btn-outline-dark">Close</Link> #4 </p> <hr/> </div> ) }
1) This returns the JSX to be displayed in the "Main" element. We are also creating three buttons.
2&4) Two are links to other URLs which the router will use to determine the appropriate component to call.
3) There is also a button with an onClick attribute. That's a React event that will call the specified handler function, handleDelete(), when the button is clicked.
HandleDelete:
handleDelete() { #1 axios.delete(`http://localhost:3001/api/articles/${this.props.match.params.id}.json`) #2 .then(() => { this.props.history.push("/articles") #3 }) .catch(error => console.log('error', error)); }
1) When the user clicks the "Delete" button, the onClick event calls the handleDelete handler function.
2) We use the Axios library to send a delete request to the provided URL.
3) We use ES6 promises when the request is complete to call the articles route. This.props.history.push("/articles")
seems like an odd line of code for what is essentially a redirect. Here's the logic. We saw earlier in Chrome Dev Tools that the props contained three objects: history, location, and match. The history object contains a stack of the URL locations visited with the most recent on top, including the current path at the very top. "Push" is a JavaScript method that adds an item to the end of an array, so pushing the articles route to the end (top) of the history stack will make that route the current location. Ta-dah.
Now go to the web page, click on an article. Then click the cancel button. It should take you back to the Article List page. Select another article and this time click delete. Poof, it's gone. Or should be if everything is working correctly. And that means you have both read and write access to the API. You should now be back to the articles list page minus one article.
Since we deleted one of our informative articles, written by the Roman philosopher Cicero no less, we might as well create one to replace it. Let's add that capability. Populate the ArticleAdd.jsx file with the below.
import React, { Component } from 'react'; import { post } from 'axios'; class ArticleAdd extends React.Component { constructor() { super(); this.state = { title: '', content: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleCancel = this.handleCancel.bind(this); } handleSubmit(event) { event.preventDefault(); post('http://localhost:3001/api/articles.json', this.state) .then((response) => { this.props.history.push(`/articles/${response.data.id}`); }) .catch(error => console.log('error', error)); } handleChange(event) { this.setState({ [event.target.name]: event.target.value }); } handleCancel() { this.props.history.push("/articles"); } render() { return ( <div> <h1>Create Article Post</h1> <form onSubmit={this.handleSubmit}> <div className="form-group"> <label>Title</label> <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" /> </div> <div className="form-group"> <label>Content</label> <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" /> </div> <div className="btn-group"> <button type="submit" className="btn btn-dark">Create</button> <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button> </div> </form> </div> ); } } export default ArticleAdd;
Test it out to make sure it works. From the Articles List page click on the Create Article button. There should be a form. First hit the cancel button to make sure you go back to the Articles list page. Click Create Article again and this time write an article. Write about your first family vacation and what childhood lessons you learned from it. Then click create and you should be taken to your new article info page. Nice! But, how did we get here?
The Imports:
import React, { Component } from 'react'; import { post } from 'axios'; #1
1) Import the post method from Axios.
ArticleAdd component's constructor function:
class ArticleAdd extends React.Component { constructor() { super(); this.state = { title: '', content: ''}; #1 this.handleChange = this.handleChange.bind(this); #2 this.handleSubmit = this.handleSubmit.bind(this); #3 this.handleCancel = this.handleCancel.bind(this); #4 }
1) We set the state to an object with properties that align with our form fields, set to empty values. By doing so we make these controlled components which you can read about in the React Docs if you want to know what that means.
2-4) We also need to bind the event handler functions that we'll be defining below.
Let's jump down to the render method.
Render:
render() { return ( #1 <div> <h1>Create Article Post</h1> <form onSubmit={this.handleSubmit}> #4a <div className="form-group"> <label>Title</label> <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" /> #2 </div> <div className="form-group"> <label>Content</label> <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" /> #3 </div> <div className="btn-group"> <button type="submit" className="btn btn-dark">Create</button> #4b <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button> </div> </form> </div> ); } }
1) The render function returns JSX to display our form.
2&3) There is an input field and a text field. React treats text fields as a self closing element just like input fields. Notice each field has a name attribute, value attribute, and an onChange attribute. When the user types a character in the field it triggers the onChange event which calls the handleChange hander function. That will update the state which will change the value attribute in the form field. That happens after every character is typed.
4a and 4b) The form element has an onSubmit attribute which calls the handleSubmit handler function when the user clicks the submit button.
HandleChange:
handleChange(event) { this.setState({ [event.target.name]: event.target.value }); #1 }
1) When the handleChange handler function is called, the event object includes the target (i.e., the form field element) which has attributes for name and value. That changes the state on whatever field the user is typing in. You can see this in action by adding console.log(event.target);
to the handleChange function then look at the console after typing in a character. You can also look at the React tab in the console after drilling down to the ArticleAdd component and you will see State update after every key is pressed.
HandleSubmit:
handleSubmit(event) { #1 event.preventDefault(); #2 post('http://localhost:3001/api/articles.json', this.state) #3 .then((response) => { this.props.history.push(`/articles/${response.data.id}`); #4 }) .catch(error => console.log('error', error)); }
1) When the user clicks the form's submit button, it triggers the onClick event which calls the handleSubmit handler function.
2) Normally when an HTML form is submitted a new page is called. Since we are sending the data via AJAX and don't want to be sent to a new page we need add preventDefault().
3) We'll sent a POST request to the API endpoint sending the current state.
4) Then using ES6 promises, when the new article is returned we'll use the response.data object to get the id so we can redirect to the correct ArticleInfo route.
Only one more component to go. Here's the code.
import React from 'react'; import { get, patch } from 'axios'; class ArticleEdit extends React.Component { constructor() { super(); this.state = { title: '', content: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleCancel = this.handleCancel.bind(this); } componentDidMount() { get(`http://localhost:3001/api/articles/${this.props.match.params.id}.json`) .then((response) => { this.setState(response.data); }) .catch(error => console.log('error', error)); } handleSubmit(event) { event.preventDefault(); patch(`http://localhost:3001/api/articles/${this.state.id}.json`, this.state) .then(() => { this.props.history.push(`/articles/${this.state.id}`); }) .catch(error => console.log('error', error)); } handleChange(event) { this.setState({ [event.target.name]: event.target.value }); } handleCancel() { this.props.history.push(`/articles/${this.state.id}`); } render() { return ( <div> <h1>Edit {this.state.title}</h1> <form onSubmit={this.handleSubmit}> <div className="form-group"> <label>Title</label> <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" /> </div> <div className="form-group"> <label>Content</label> <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" /> </div> <div className="btn-group"> <button type="submit" className="btn btn-dark">Update</button> <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button> </div> </form> </div> ); } } export default ArticleEdit;
Save it, then edit a few articles to make sure it works.
This component is mostly the same as the ArticleAdd component plus parts of the ArticleInfo component, so going through the code should reinforce what we covered earlier.
The imports:
import React from 'react'; import { get, patch } from 'axios'; #1
1) We import the get and patch methods from Axios. You could use put instead of patch since our Rails API routes them both to the same controller action.
The ArticleEdit component constructor:
class ArticleEdit extends React.Component { constructor() { super(); this.state = { title: '', content: ''}; #1 this.handleChange = this.handleChange.bind(this); #2 this.handleSubmit = this.handleSubmit.bind(this); this.handleCancel = this.handleCancel.bind(this); }
1) Set the initial state to an object with properties that align to the form fields, set to empty values.
2) Bind all the event handler functions.
ComponentDidMount:
componentDidMount() { #1 get(`http://localhost:3001/api/articles/${this.props.match.params.id}.json`) #2 .then((response) => { this.setState(response.data); #3 }) .catch(error => console.log('error', error)); }
1) ComponentDidMount is a React lifecycle method. This method is identical to the one we have in ArticlesInfo.
2) We need to do an AJAX call to the API to get the data for the existing record.
3) Using promises, when we get the response we apply the setState method to change the state from the empty object we declared in the constructor, to the one from the database.
Render:
render() { return ( #1 <div> <h1>Edit {this.state.title}</h1> #2 <form onSubmit={this.handleSubmit}> #3a <div className="form-group"> <label>Title</label> <input type="text" name="title" value={this.state.title} onChange={this.handleChange} className="form-control" /> #4 </div> <div className="form-group"> <label>Content</label> <textarea name="content" rows="5" value={this.state.content} onChange={this.handleChange} className="form-control" /> #5 </div> <div className="btn-group"> <button type="submit" className="btn btn-dark">Update</button> #3b <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button> #6 </div> </form> </div> ); }
1) Our render method will return a JSX form block.
2) Start with an h1 element and insert the title from the current state.
3a&b) The form element has an onSubmit event that calls the handleSubmit handler function when the user clicks the submit button.
4&5) We have input and textarea elements. HTLM input elements are self-closing while textarea elements have opening and closing tags. React treats them both as self-closing elements. Both elements have a value attribute that is equal to the current state for title and content respectively. And they each have an onChange attribute that calls the handleChange handler function.
6) A cancel button calls the handleCancel handler function.
HandleChange:
handleChange(event) { #1 this.setState({ [event.target.name]: event.target.value }); #2 }
1) The handleChange handler function is called every time the user types a character into one of the form fields. We are passing the event as the argument so we know which element to set the state for.
2) Set the state with the new value. If we only had one input, say for the title, then this line would be: this.setState({ title: event.target.value });
. But since it's called from different form fields we need to pull the attribute name from the event object, as well as the value.
HandleSubmit:
handleSubmit(event) { #1 event.preventDefault(); #2 patch(`http://localhost:3001/api/articles/${this.state.id}.json`, this.state) #3 .then(() => { this.props.history.push(`/articles/${this.state.id}`); #4 }) .catch(error => console.log('error', error)); #5 }
1) When the user clicks the submit button in the form element, the onSubmit event calls the handleSubmit handler function.
2) When an HTML form is submitted, by default it tries to load a new page. But since our form is being submitted with AJAX we don't want a new page. So we call the preventDefault method on the event.
3) Send the AJAX request with the patch method to the endpoint url provided, sending the form data from this.state.
4) Using promises we wait from confirmation that the data was successfully received and saved, then call that article's ArticleInfo page.
5) If we instead get an error, we'll log it to the console.
HandleCancel:
handleCancel() { #1 this.props.history.push(`/articles/${this.state.id}`); #2 }
1) If the user clicks the cancel button, the onClick event calls the handleCancel handler function.
2) Pushing the URL path with the current article id to the top of the history stack will trigger a redirect to that route, taking us back to the ArticleInfo page for that article.
Rather than changing into the client directory to start the React app, you can use the change-working-directory option to run it from the Rails product root directory like this: yarn --cwd client start
. That still requires you to open two Terminal windows and start two separate servers. Instead, you can add the Foreman gem to run them both with one command. Foreman is a utility you call from the Terminal so you save it directly to your system, not though your Gemfile.
gem install foreman
Then create a Procfile for your development environment and populate it with the below.
touch Procfile.dev
# Procfile.dev web: cd client && PORT=3000 yarn start api: PORT=3001 && bundle exec rails s
By default Foreman looks for a file called Procfile to execute. The force option -f tells Foreman to use the provided file name instead. To start both servers run:
foreman start -f Procfile.dev
If you want to get even fancier you could create a rake task to execute Procfile.dev. For that you need to add the foreman gem to your Gemfile and specify the version (older versions don't recognize the cd command). At the time of this writing it was necessary to also specify the Thor dependency gem with version 0.19.1.
# Gemfile group :development do gem 'foreman', '~> 0.85.0' gem 'thor', '~> 0.19.1' end
bundle install
Rails has a generator to add custom rake tasks. We'll call our task "start". Custom rake tasks are added to the lib/tasks folder. Our task file name will be start.rake.
rails generate task start
And Populate it with:
# lib/tasks/start.rake desc 'Start development servers' namespace :start do exec 'foreman start -f Procfile.dev' end
Now we can start both servers with:
rake start
We are done! We now have a fully functioning CRUD application with a Ruby on Rails API as the back end and a separate React application as the front end. Victory lap.
What's next? How about: Add Redux to our app; Deploy our app to Heroku; Combine React with Rails using Webpacker