Integrate React into an API built with Node.js, Express and MongoDB.
By Steve Carey - 7/9/2019
Part 1 of this tutorial: Build an API with Node.js, Express and MongoDB Tutorial
Finished code: Github
This is part two of a tutorial on building a MERN web app. In this tutorial we will integrate a React client into an API built with Node.js, the Express web framework and the MongoDB database. Part 1 is linked above. There is an optional part 3 where we deploy the app to Heroku.
In February 2018 React version 16.8 was released which incorporates the new Hooks API. It has some significant syntactic changes, specifically the use of state and lifecycle changes in functional components. These changes are completely optional so the below sytax is completely valid. To see this same tutorial using React Hooks go to MERN APP: Integrate React with Hooks
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 Node.js with Express API 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 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 and run the server.
Inside your 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. You can read about it at docs.npmjs.com - package.json.
Install the react-router-dom (for routing), axios (for AJAX requests), and bootstrap packages. The bootstrap package is a popular CSS library that we'll use for convenience and discuss a bit more in the stylesheets section.
yarn add react-router-dom axios bootstrap
Now if you look back at the packages.json file you should see those packages as dependencies.
Let's make one more change to our package.json file. Add a proxy property set to our API's port. This will automatically be added to the API routes we call from our React front end, so we only have to use the paths, not the full URLs in our code. There are other ways we could accomplish the same thing, but this makes it easier if we deploy our app to a platform like Heroku.
Client/package.json File
{
"name": "client",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:3001",
"dependencies": {
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.1",
"react-scripts": "3.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
...
}
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 you can use these UNIX commands to add and remove the relevant directories and files. Or do it from your text editor if you prefer.
rm README.md rm -rf .git mkdir -p src/components/pages touch src/components/pages/Home.js mkdir src/components/articles touch src/components/articles/ArticleList.js touch src/components/articles/ArticleInfo.js touch src/components/articles/ArticleAdd.js touch src/components/articles/ArticleEdit.js
We removed some unnecessary files including the git repository that create-react-app generated for us. We won't be using git in this tutorial but if we were we would generate a git repository in the project root directory for our API and our React client combined. You can leave the .gitignore file though since git allows multiple gitignore files.
We left the logo.svg file for now so our app doesn't crash. You can delete it at the end.
The index.css file is for our app-wide css. If you want to add custom CSS classes that apply to the whole app this is where you would do it. 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.
// src/stylesheets/App.css
/* Remove all the CSS classes */
To add css classes that apply only to a particular component you could create a css file in the same folder as that component and import it in the component(s) that use it.
Bootstrap: For convenience we're using Bootstrap for our styling. We installed the bootstrap package earlier. Now import it to our index.js file.
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';
import App from './App';
...
If you want to use the JavaScript features of Bootstrap like drop-down menus you can also install the react-bootstrap package, be we won't use those in this tutorial.
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.
In the src/index.js file we import the React library and render our main component. Other than adding the Bootstrap import above, we don't need to make any changes to the src/index.js file. It should look like this.
// src/index.js import React from 'react'; #1a import ReactDOM from 'react-dom'; #1b import 'bootstrap/dist/css/bootstrap.css'; import './index.css'; import App from './App'; #1c import * as serviceWorker from './serviceWorker'; ReactDOM.render(<App />, document.getElementById('root')); #2 serviceWorker.unregister(); #3
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. We need components to correspond with the API's endpoints to perform the CRUD actions.
A few notes on syntax. We can use all the latest JavaScript syntax without worrying about browser compatibility because create-react-app installed Babel which compiles our JavaScript into ES5. We did not add the step of installing Babel in our API, so while we could still use most of the ES6 syntax, there are some features not yet supported by Node like the Import and Export syntax. But we can and will be using those here. Also, in general I am using the declaration syntax for functions rather than arrow functions just to make it explicit when you are returning a value. But there is no reason not to use arrow functions if you prefer it.
The App.js component file holds the structure for our web page. The ArticleList, ArticleInfo, ArticleAdd, and ArticleEdit components represent views that correspond to the Read, Read and Delete, Create, and Update actions respectively. We'll also add a Home component to be our home page. Note that component names must be capitalized. In each file we will build one React component, with the exception of App.js where we build 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.
Before we build our full CRUD app let's start out with a very simple component called Home. We already created an empty Home.js file. Populate it with the below:
// src/components/pages/Home.jsx import React from 'react'; #1 function 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/pages/Home'; ... ReactDOM.render(<Home />, document.getElementById('root'));
Undo those index.js file changes back to the way it was. Referencing "App."
import App from './App'; ... ReactDOM.render(<App />, document.getElementById('root'));
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.
The top level 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.js file all at once:
// src/App.jsx
import React from 'react';
import {BrowserRouter as Router, Route, NavLink, Switch} from 'react-router-dom';
import './App.css';
import Home from './components/pages/Home';
import ArticleList from './components/articles/ArticleList';
import ArticleInfo from './components/articles/ArticleInfo';
import ArticleAdd from './components/articles/ArticleAdd';
import ArticleEdit from './components/articles/ArticleEdit';
class App extends React.Component {
render() {
return (
<div className="App">
<Router>
<Navigation />
<div className="container">
<Main />
</div>
</Router>
</div>
);
}
}
function Navigation() {
return(
<nav className="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div className="container">
<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>
<div>
</nav>
);
}
function Main() {
return(
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/articles" component={ArticleList} />
<Route exact path="/articles/new" component={ArticleAdd} />
<Route exact path="/articles/:<%= @mongoid %>id" component={ArticleInfo} />
<Route exact path="/articles/:<%= @mongoid %>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 were to click on the articles link you'll get an error because we haven't populated that yet.
This file actually contains three separate components... App, Navigation, and Main. But most of this code relates to 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 from 'react'; #1 import {BrowserRouter as Router, Route, NavLink, Switch} from 'react-router-dom'; #2 import './App.css'; import Home from './components/pages/Home'; #3 import ArticleList from './components/articles/ArticleList'; import ArticleInfo from './components/articles/ArticleInfo'; import ArticleAdd from './components/articles/ArticleAdd'; import ArticleEdit from './components/articles/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.
The App class component's render method:
class App extends React.Component { render() { return ( <div className="App"> <Router> #1 <Navigation /> #2 <div className="container"> <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:
function Navigation() { return( #1 <nav className="navbar navbar-expand-lg navbar-dark bg-dark mb-4"> <div className="container"> <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> <div> </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:
function Main() { #1 return( <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/:<%= @mongoid %>id" component={ArticleInfo} /> <Route exact path="/articles/:<%= @mongoid %>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/articles/ArticleList.jsx
import React from 'react';
import { get } from 'axios';
import { Link } from 'react-router-dom';
class ArticleList extends React.Component {
constructor() {
super();
this.state = { articles: [] };
}
componentDidMount() {
get('/api/articles')
.then(response => {
this.setState({articles: response.data});
})
.catch(error => console.log('error', error));
}
render() {
return (
<div>
<h2>
Articles
<Link to="/articles/new" className="btn btn-primary float-right">Create Article</Link>
</h2><hr/>
{this.state.articles.map(function(article) {
return(
<div key={article.<%= @mongoid %>id}>
<h2><Link to={`/articles/${article.<%= @mongoid %>id}`}>{article.title}</Link></h2>
<small>{article._id}</small>
<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 Node/Express API application, make sure the server is running on port 3001. If you have a local MongoDB database make sure that is running, or start it in a separate terminal window (any directory will work):
mongod
To start the API server in a separate terminal window go to the project's root directory:
nodemon
And with all the changes we made you may need to restart the React app. From the terminal window running your React app:
CTRL+C
yarn start
Now if you go to the browser and click on the Articles link you should see the list of articles you created for the API. This corresponds with the JSON view at http://localhost:3001/api/articles 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 from 'react'; #1 import { get } from 'axios'; #2 import { Link } from 'react-router-dom'; #3
1) Import React.
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 React.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> <h2> Articles <Link to="/articles/new" className="btn btn-primary float-right">Create Article</Link> #2 </h2><hr/> {this.state.articles.map(function(article) { #3 return( <div key={article.<%= @mongoid %>id}> #4 <h2><Link to={`/articles/${article.<%= @mongoid %>id}`}>{article.title}</Link></h2> #5 <small>{article._id}</small> #6 <hr/> </div> ) })} </div> ) }
1) The render method returns JSX listing each article. 2) Include a link to the new articles route.
3) 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.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 corresponds to our API's Get /api/articles endpoint. Populate the file with the below and save it.
// src/components/articles/ArticleInfo.jsx
import React from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
class ArticleInfo extends React.Component {
constructor() {
super();
this.state = { article: {} };
this.handleDelete = this.handleDelete.bind(this);
}
componentDidMount() {
axios.get(`/api/articles/${this.props.match.params.<%= @mongoid %>id}`)
.then((response) => {
this.setState({
article: response.data
})
})
.catch(error => console.log('error', error));
}
handleDelete() {
axios.delete(`/api/articles/${this.props.match.params.<%= @mongoid %>id}`)
.then(() => {
this.props.history.push("/articles")
})
.catch(error => console.log('error', error));
}
render() {
return (
<div>
<h2>{this.state.article.title}</h2>
<small>_id: {this.state.article.<%= @mongoid %>id}</small>
<p>{this.state.article.content}</p>
<p className='btn-group'>
<Link to={`/articles/${this.state.article.<%= @mongoid %>id}/edit`} className="btn btn-info">Edit</Link>
<button onClick={this.handleDelete} className="btn btn-danger">Delete</button>
<Link to="/articles" className="btn btn-secondary">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.
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(`/api/articles/${this.props.match.params.<%= @mongoid %>id}`) #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/:<%= @mongoid %>id), the url (articles/2), and another object called params. Params contains a single path param of :<%= @mongoid %>id. So to get the article id we need to chain this all together with this.props.match.params.<%= @mongoid %>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.title}</h2> <small>_id: {this.state.article.<%= @mongoid %>id}</small> <p>{this.state.article.content}</p> <p className='btn-group'> <Link to={`/articles/${this.state.article.<%= @mongoid %>id}/edit`} className="btn btn-info">Edit</Link> #2 <button onClick={this.handleDelete} className="btn btn-danger">Delete</button> #3 <Link to="/articles" className="btn btn-secondary">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(`/articles/${this.props.match.params.<%= @mongoid %>id}`) #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 (yes he is responsible for lorem ipsum), we might as well create one to replace it. Let's add that capability. Populate the ArticleAdd.jsx file with the below.
// src/components/articles/ArticleAdd.jsx
import React 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('/api/articles', this.state)
.then((response) => {
this.props.history.push(`/articles/${response.data.<%= @mongoid %>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</h1>
<hr/>
<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-primary">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 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</h1></hr> <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-primary">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('/api/articles', this.state) #3 .then((response) => { this.props.history.push(`/articles/${response.data.<%= @mongoid %>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.
// src/components/articles/ArticleEdit.jsx
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(`/api/articles/${this.props.match.params.<%= @mongoid %>id}`)
.then((response) => {
this.setState(response.data);
})
.catch(error => console.log('error', error));
}
handleSubmit(event) {
event.preventDefault();
patch(`/api/articles/${this.state.<%= @mongoid %>id}`, this.state)
.then(() => {
this.props.history.push(`/articles/${this.state.<%= @mongoid %>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.<%= @mongoid %>id}`);
}
render() {
return (
<div>
<h1>Edit {this.state.title}</h1>
<hr/>
<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-primary">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(`/api/articles/${this.props.match.params.<%= @mongoid %>id}`) #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 <hr/> <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-primary">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(`/api/articles/${this.state.<%= @mongoid %>id}`, this.state) #3 .then(() => { this.props.history.push(`/articles/${this.state.<%= @mongoid %>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.<%= @mongoid %>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.
Right now we need separate terminal windows open to run our back end API server (port 3001) and our front end React app (port 3000). Not to mention our MongoDB server. For convenience we can run our API and React apps with one command using the Concurrently package. Make sure you are in the project's root directory (not in the client folder). Using the --dev flag installs it as a development environment dependency.
npm install concurrently --save-dev
// package.json
...
"scripts": {
"start": "node server.js",
"dev": "concurrently \"nodemon server.js\" \"cd client && npm run start\""
},
...
"devDependencies": {
"concurrently": "^4.1.1"
},
This will run the nodemon command on the API application starting the server on port 3001. Then it will cd to the client directory and run the start command which will run the React app on port 3000. To execute this script, make sure you stop both servers first, then from the project's root directory run:
npm run dev
This should start both servers and open the React app in your browser.
Well now we have a fully functioning MERN app. To put it in production one option is to use Heroku. To find out how to do that go to the final part of this MERN tutorial series Deploy a MERN app to Heroku