Integrate React with Hooks into an API built with Node.js, Express and MongoDB. This is a full Create-Read-Update-Delete (CRUD) app with a corresponding RESTful API.
By Steve Carey - 7/18/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 a MongoDB database. Part 1 is linked above. There is an optional part 3 where we deploy the app to Heroku. It incorporates all the CRUD database actions of Creating records, Reading records, Updating records, and Deleting records. It uses a RESTful API which essentially means that the API's endpoints correspond with the CRUD actions.
The Hooks API is a new feature in React as of version 16.8 (released February 2019). Hooks allow you to use state and lifecycle methods in functional rather than class components for shorter, cleaner code. To read more about React hooks go to reactjs.org/docs/hooks-intro.html. For this same tutorial without hooks and all the fun that goes along with componentDidMount, "this", bind, and more see Build a MERN App: Integrate React.
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 an 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
Poof... we now have a working React app ready to go. Start the server:
yarn start
And this should open your browser to localhost:3000 and greet you with a Welcome to React web page including a spinning logo. We're in business.
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 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 akin to the Rails Gemfile. " if @rails %> 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.
Make one more change to the package.json file. Add a proxy property set to your 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 you deploy your app to a platform like Heroku. And we won't need to use the cors package in our API.
# client/package.json
{
"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.js
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';
function App() {
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 do get an error try restarting the server: CTRL+C yarn start
. If you click on the articles link it will give you an error because we haven't populated that yet.
This file actually contains three separate functional 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 "bootstrap/dist/css/bootstrap.min.css"; #3 import '../stylesheets/App.css'; import Home from './components/pages/Home'; #4 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 Bootstrap CSS library we added.
4) Import the other components that we will be calling with our Routes. Each of those files will contain a functional component.
The App component's return statement:
function App() { return ( <div className="App"> <Router> #1 <Navigation /> #2 <div className="container"> <Main /> #3 </div> </Router> </div> ); }
1) In the App function's return statement 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() { #1 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"> #2 <NavLink exact className="nav-link" activeClassName="active" to="/">Home</NavLink> </li> #3 <li className="nav-item"> <NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink> </li> </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.
Note that throughout this tutorial I will be using the function declaration syntax rather than function expressions or arrow functions, unless there is a specific reason those should be used. But if you prefer arrow function syntax feel free to change it.
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.js file<%= " which will be our React equivalent to the Rails index.html.erb file" if @rails %>. Populate it with the ArticleList component below and save it.
// src/components/articles/ArticleList.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
function ArticleList() {
const [articles, setArticles] = useState([])
useEffect(function() {
async function getArticles() {
try {
const response = await axios.get("/api/articles");
setArticles(response.data);
} catch(error) {
console.log('error', error);
}
}
getArticles();
}, []);
return (
<div>
<h2>
Articles
<Link to="/articles/new" className="btn btn-primary float-right">Create Article</Link>
</h2>
<hr/>
{articles.map((article) => {
return(
<div key={article.<%= @mongoid %>id}>
<h4><Link to={`/articles/${article.<%= @mongoid %>id}`}>{article.title}</Link></h4>
<small>_id: {article.<%= @mongoid %>id}</small>
<hr/>
</div>
)
})}
</div>
)
}
export default ArticleList;
<% if @rails %>Let's see if it works. Make sure your backend API's server is running on port 3001. In the API app's project root directory run:
rails s -p 3001
Let's see if it works. 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
Now if you go to the browser and click on the Articles link you should see a list of articles from the database. 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 back-end API app and one for the React front-end app.
Time to break it down starting with...
The imports:
import React, { useState, useEffect } from 'react'; #1 import { get } from 'axios'; #2 import { Link } from 'react-router-dom'; #3
1) React components require the React library. Additionally, import useState and useEffect to use the State and Effect hooks. Read Hooks at a Glance for a quick summary.
2) To access data from the API we'll make an AJAX get request using 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.
function ArticleList() {UseState Hook:
const [articles, setArticles] = useState([])
Set the initial state of our component to an empty array with the useState hook. UseState is a two element array that contains the current state as the first element and a function to update it as the second. Here we're assigning the (const) variable "articles" to the current state value, and "setArticles" to the update function.
useEffect(function() { #1 async function getArticles() { #2 try { const response = await get("/api/articles"); #3 setArticles(response.data); #4 } catch(error) { console.log('error', error); } } getArticles(); }, []); #5
1) UseEffect essentially replaces the lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount). Place your asynchronous code here such as your API call. Code in the useEffect method runs after the initial render of the component and changes the state of the component.
2) The API call is asynchronous since it sends a get request to the API and waits for a response from the server.
3) We're using the Axios library to make an AJAX get request and convert the JSON response to a JavaScript object. We aren't including the domain part of the URL -- http://localhost:3001 -- because we added it as a proxy in the package.json file.
4) Call the setArticles method to change the articles state, passing in the response object we got back from the API (which is an array of article objects). The React useState hook will change the articles state to this object.
5) The useEffect hook takes a second argument that instructs React to rerun the effect. We only want to run the effect once to get the list of articles so just set it to an empty array.
We used the async/await Promises syntax. If you want to use the older Promises syntax it would look something like this and would behave the same:
useEffect(function() { get("/api/articles") .then(function (response) { setArticles(response.data); }) .catch(function (error) { console.log('error', error); }); }, []);
Return statement:
return ( #1 <div> {articles.map((article) => { #2 return( <div key={article.<%= @mongoid %>id}> #3 <h2><Link to={`/articles/${article.<%= @mongoid %>id}`}>{article.title}</Link></h2> #4 <small>{article.<%= @mongoid %>id}</small> <hr/> </div> ) })} <Link to="/articles/new" className="btn btn-outline-primary">Create Article</Link> #5 </div> )
1) React components return JSX which is converted to HTML.
2) Access the articles state object which 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 id, 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 console.log(n, articles);
statements to your component with n being sequential numbers. Refresh your browser and open Chrome Developer Tools - Inspect and go to the Console tab. There you can see the console logs of the data pulled and the order the code is executed.
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.js import React, { useState, useEffect } from "react"; import axios from 'axios'; #1 import { Link } from 'react-router-dom'; function ArticleInfo(props) { const [article, setArticle] = useState({}); #2 useEffect(function() { #3a async function getArticle() { try { const response = await axios.get(`/api/articles/${props.match.params.<%= @mongoid %>id}`); #3b setArticle(response.data); #3c } catch(error) { console.log('error', error); } } getArticle(); }, [props]); #3d async function handleDelete() { #4a try { await axios.delete(`/api/articles/${props.match.params.<%= @mongoid %>id}`); #4b props.history.push("/articles"); #4c } catch(error) { console.error(error); } } return ( #5 <div> <h2>{article.title}</h2> <small>_id: {article.<%= @mongoid %>id}</small> <p>{article.content}</p> <div className="btn-group"> <Link to={`/articles/${article.<%= @mongoid %>id}/edit`} className="btn btn-primary">Edit</Link> <button onClick={handleDelete} className="btn btn-danger">Delete</button> <Link to="/articles" className="btn btn-secondary">Close</Link> </div> <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. Click the cancel button and it should take you back to the Article List page. Select another article and this time click delete. Poof, it's gone if everything is working correctly. Which means you have both read and write access to the API. You should be back to the articles list page minus one article.
Let's break down the code.
1) The imports are the same as for ArticleList. 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.
2) Call the useState hook and set the value to a variable called article with an initial state of an empty object {}. Assign the update function to the variable name setArticle.
3a) The useEffect hook is similar to ArticleList.
3b) But this time we when we make our GET request to the API we need to send the article id. 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/id), 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
.
3c) When the response to the AJAX request comes back we will use the setArticle method to assign response data from the API to the article object in the component's state.
3d) Adding props to the second argument of the useEffect hook instructs React to re-run the effect if props changes.
If you prefer to use the promise.then syntax rather than async/await, then the useEffect hook would look something like this:
useEffect(function() { axios.get(`/api/articles/${props.match.params.<%= @mongoid %>id}`) .then(function(response) { setArticle(response.data); }) .catch(function (error) { console.log('error', error); }); }, [props]);
4a) When the user clicks the "Delete" button, the onClick event calls the handleDelete handler function.
4b) We use the Axios library to send a delete request to the provided URL.
4c) When we get a response from the delete request we will redirect to the articles page using props.history.push("/articles")
. 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.
The promises.then syntax for the handleDelete function would look something like this:
function handleDelete() { axios.delete(`/api/articles/${props.match.params.<%= @mongoid %>id}`) .then(function() { props.history.push("/articles") }) .catch(function(error) { console.log('error', error); }); }
5) The component returns the JSX which is converted to HTML. It includes the article data and buttons for edit, delete and back.
We've covered Read and Delete, how about Create? Populate the ArticleAdd.jsx file with the below.
// src/components/articles/ArticleAdd.js import React, { useState } from "react"; #1a import { post } from 'axios'; #1b function ArticleAdd(props) { const initialState = { title: '', content: '' } const [article, setArticle] = useState(initialState) #2 function handleChange(event) { #3 setArticle({...article, [event.target.name]: event.target.value}) } function handleSubmit(event) { #4a event.preventDefault(); #4b if(!article.title || !article.content ) return #4c async function postArticle() { try { const response = await post('/api/articles', article); #4d props.history.push(`/articles/${response.data.<%= @mongoid %>id}`); #4e } catch(error) { console.log('error', error); } } postArticle(); } function handleCancel() { props.history.push("/articles"); } return ( #5 <div> <h1>Create Article</h1> <hr/> <form onSubmit={handleSubmit}> <div className="form-group"> <label>Title</label> <input name="title" type="text" value={article.title} onChange={handleChange} className="form-control" /> </div> <div className="form-group"> <label>Content</label> <textarea name="content" rows="5" value={article.content} onChange={handleChange} className="form-control" /> </div> <div className="btn-group"> <input type="submit" value="Submit" className="btn btn-primary" /> <button type="button" onClick={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?
1) In our React imports we will only be using the useState hook. For our Axios imports we'll be using a post request.
2) Our useState hook will create a state named article with initial state set to an object with title and content properties set to empty strings. We'll also set the function for updating state to variable "setArticle".
3) Every time a user types a character in a form input field the onChange property calls the handleChange handler function passing the event object as an implicit argument. The event object includes the target (i.e., the form field element) which has attributes for field name and value.
The handleChange function in turn calls setArticle which updates the article state with the new value. You need to include the ...article spread operator so that the new character is added to the existing article value, otherwise it will just overwrite it. You can see the process 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.
axios.post('/api/articles', article) .then(function(response) { props.history.push(`/articles/${response.data.<%= @mongoid %>id}`); }) .catch(function(error) { console.log('error', error) });
5) Our return statement is the JSX for our form. Each form field element has properties for name, value (set to the article state) and onClick set to a handler function. The form element has an onSubmit property set to a handler function that is triggered when the user clicks the submit button.
Only one more component to go. Here's the code.
// src/components/articles/ArticleEdit.js
import React, { useState, useEffect } from "react";
import { get, patch } from 'axios';
function ArticleEdit(props) {
const initialState = { title: '', content: '' }
const [article, setArticle] = useState(initialState)
useEffect(function() {
async function getArticle() {
try {
const response = await get(`/api/articles/${props.match.params.<%= @mongoid %>id}`);
setArticle(response.data);
} catch(error) {
console.log(error);
}
}
getArticle();
}, [props]);
function handleSubmit(event) {
event.preventDefault();
async function updateArticle() {
try {
await patch(`/api/articles/${article.<%= @mongoid %>id}`, article);
props.history.push(`/articles/${article.<%= @mongoid %>id}`);
} catch(error) {
console.log(error);
}
}
updateArticle();
}
function handleChange(event) {
setArticle({...article, [event.target.name]: event.target.value})
}
function handleCancel() {
props.history.push(`/articles/${article.<%= @mongoid %>id}`);
}
return (
<div>
<h1>Edit {article.title}</h1>
<hr/>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Title</label>
<input type="text" name="title" value={article.title} onChange={handleChange} className="form-control" />
</div>
<div className="form-group">
<label>Content</label>
<textarea name="content" rows="5" value={article.content} onChange={handleChange} className="form-control" />
</div>
<div className="btn-group">
<button type="submit" className="btn btn-primary">Update</button>
<button type="button" onClick={handleCancel} className="btn btn-secondary">Cancel</button>
</div>
</form>
</div>
);
}
export default ArticleEdit;
Save it, then edit an article to make sure it works.
This component is essentially a combination of the ArticleInfo and ArticleAdd components. So it would be a good exercise for you to go through the lines of code and make sure you understand what they are doing.
This completes the React portion of this app!
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 you have a fully functioning MERN app using React Hooks. To put it in production one option is to use Heroku. To find out how to do that go to the optional final part of this MERN tutorial series Deploy a MERN app to Heroku