Integrate React and Redux 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/19/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 with Redux front end application 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. 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.
This tutorial uses the Hooks API which is a new feature in React as of version 16.8 (released February 2019) and React-redux as of version 7.1 (released June 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.
This tutorial assumes you are familiar with React but not necessarily with React hooks. It assumes only limited knowledge of Redux.
We will install our react app in a directory called "client" inside our API's root directory. Use create-react-app to set up a React app.
create-react-app client
cd client
Install your packages:
yarn add redux react-redux redux-thunk redux-logger react-router-dom axios bootstrap
Run the server to make sure it's working:
yarn start
You should see the default Welcome to React app in your browser.
Since we will be integrating our React front end with our API let's add the API's URL as a proxy in the client/package.json file. This will give us three benefits. Whenever we call the API in our React app code we only need to list the path, not the full URL. Second, if you want to change the API domain for any reason, you only need to do it in one place. Third, it helps eliminate the cross-origin issues discussed in the API tutorial.
Make sure to put this in the package.json file in the client directory not the one in the project's root directory.
// client/package.json
{
"name": "client",
"version": "0.1.0",
"private": true,
"proxy": "http://localhost:3001",
"dependencies": {
"axios": "^0.19.0",
...
}
Use the below Unix commands to create directories and remove, move, and create files. Or you can do it in you text editor.
rm README.md rm -rf .git mkdir -p src/components/pages touch src/components/pages/About.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 mkdir src/actions touch src/actions/index.js mkdir src/reducers touch src/reducers/index.js touch src/reducers/articlesReducer.js touch src/reducers/articleReducer.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'll cover the added directories and files as we build the app but just as a high level overview, Redux uses actions and reducers. Our app is just big enough to warrant separate folders for each. And each has an index.js file which is automatically called by Redux when you reference the folder.
Go to the src/App.css file and delete all the CSS there so it doesn't mess with our app's styling.
First we'll set up the main index.js file for our React with Redux app.
// src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import logger from 'redux-logger' import { Provider } from 'react-redux'; import 'bootstrap/dist/css/bootstrap.css'; import './index.css'; import App from './App'; import rootReducer from './reducers'; const store = createStore(rootReducer, applyMiddleware(thunk, logger)); #1 ReactDOM.render( <Provider store={store}> #2 <App /> </Provider>, document.getElementById('root') );
The first argument in createStore() is the reducer. Our app will have two reducer functions, one for an array of articles, and one for a specific article. When you have more than one reducer you need to combine them into a single reducer object using Redux's combineReducers method. We can set that up now in the reducers index file even though we haven't defined the reducers yet.
// src/reducers/index.js
import { combineReducers } from 'redux';
import articles from './articlesReducer';
import article from './articleReducer';
export default combineReducers({
articles: articles,
article: article,
});
React Router is the most popular Routing package for React. The docs are at reacttraining.com/react-router/core/guides. It can work independently of Redux.
Our navigation will be contained in the App component. We will set up a nav bar with two menu items, home and articles, and add all our routes up front.
// src/App.js
import React from 'react';
import {BrowserRouter as Router, Route, NavLink, Switch} from 'react-router-dom';
import './App.css';
import About from './components/pages/About';
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="/">Articles</NavLink></li>
<li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/about">About</NavLink></li>
</ul>
</div>
</nav>
);
}
function Main() {
return(
<Switch>
<Route exact path="/" 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} />
<Route exact path="/about" component={About} />
</Switch>
);
}
export default App;
Our home page here is the ArticleList component.
To test if our initial Redux app and router are working let's add our About component. This will simply display "About Page". We already created an empty About.js file. Populate it with the below:
// src/components/pages/About.jsx import React from 'react'; 1 function About() { 2 return ( <div className="jumbotron"> <h1>About Page</h1> </div> ); } export default About; 3
And let's create a placeholder for the ArticleList page:
// src/components/articles/ArticleList.js
import React from 'react';
export default function ArticleList() { return <h1>Articles</h1> }
Restart the server: CTRL+C yarn start
Now when you go to localhost:3000 you should you should be on the articles page with a navbar with routes for Articles and About. Clicking on About will take you to the About page displaying a jumbotron that says "About Page". And clicking on Articles should take you back to the ArticleList page. Our routes are working! Now it's time to create our first API call.
Let's make our first API call and update our User Interface with our first Action. From an HTTP request perspective, this is a GET request. From a database CRUD perspective this is a Read command.
Let's initially populate our app with a list of articles pulled from the database and served by the API. Since we set up a Redux store object to be our single source of truth, and since the ArticleList page is our root route, let's add two lines to our initial src/index.js file:
// src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import logger from 'redux-logger' import { Provider } from 'react-redux'; import 'bootstrap/dist/css/bootstrap.css'; import './index.css'; import App from './App'; import rootReducer from './reducers'; import { setArticles } from './actions'; #1 const store = createStore(rootReducer, applyMiddleware(thunk, logger)); store.dispatch(setArticles()); #2 ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
The setArticles method includes our first Action. Since we will have several actions we will define them all in the actions folder.
// src/actions/index.js import { get } from 'axios'; #1 export const SET_ARTICLES = 'SET_ARTICLES'; #2 export function setArticles() { #3 return function(dispatch) { return get("/api/articles") #4 .then(function(response) { dispatch({type: SET_ARTICLES, articles: response.data}) #5 }) .catch(function(error) { console.log('error', error); }); }; };
Let's break this down.
Now that we've sent our retrieved articles to the reducer with our action, let's define the reducer.
// src/reducers/articlesReducer.js import { SET_ARTICLES } from '../actions'; #1 const initialState = { articles: [] } export default function articlesReducer(state = initialState, action) { #2 switch (action.type) { case SET_ARTICLES: #3 return action.articles; default: #4 return state; } }
Now our store contains an an articles array with the articles from the API. Time to use that to change the User Interface. Fill in the ArticleList component with this:
// src/components/articles/ArticleList.js import React from 'react'; import { useSelector } from 'react-redux'; #1 import { Link } from 'react-router-dom'; function ArticleList(){ const articles = useSelector(function(state) { return state.articles }); #2 return ( #3 <div> <h2> Articles <Link to="/articles/new" className="btn btn-primary float-right">Create Article</Link> </h2> {articles.length && articles.map(function(article) { #4 return ( <div key={ article.<%= @mongoid %>id }> #5 <hr/> <h4><Link to={`/articles/${article.<%= @mongoid %>id}`}>{article.title}</Link></h4> #6 <small>id: {article.<%= @mongoid %>id}</small> </div> ); })} </div> ) } export default ArticleList;
articles.length
. If that evaluates to false then it won't go on to the articles.map method.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
You should see the articles you created in the API tutorial. If it isn't working first try restarting the client server: CTRL+C yarn start
.
On to the ArticleAdd component. This component will display a form to submit a new article. We will send an HTTP POST request to the appropriate API endpoint, which should Create a new record in the database. Then we'll update the Redux store to add the new article to the articles array and redirect our route back to the ArticleList page.
This time everything begins in the component, which is a form. Populate it with the below:
// src/components/articles/ArticleAdd.js import React, { useState } from 'react'; #1 import { useDispatch } from 'react-redux'; #2 import { post } from 'axios'; import { addArticle } from '../../actions'; #3 function ArticleAdd(props) { #4 const initialState = { title: '', content: '' } const [article, setFields] = useState(initialState) #5 const dispatch = useDispatch(); #6 function handleChange(event) { #7 setFields({...article, [event.target.name]: event.target.value}); } function handleSubmit(event) { #8 event.preventDefault(); if(!article.title || !article.content ) return post('/api/articles', {title: article.title, content: article.content}) .then(function(response) { dispatch(addArticle(response.data)); }) .then(function() { props.history.push("/") }) .catch(function(error) { console.log(error); }); }; function handleCancel() { props.history.push("/"); } return ( <div> <h4>Add Article</h4> <form onSubmit={ handleSubmit }> <div className="form-group"> <input type="text" name="title" required value={article.title} onChange={handleChange} className="form-control" placeholder="Title" /> </div> <div className="form-group"> <textarea name="content" rows="5" required value={article.content} onChange={handleChange} className="form-control" placeholder="Content" /> </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;
Time to define our addArticle function. We'll add it to the actions folder.
// src/actions/index.js
...
export const ADD_ARTICLE = 'ADD_ARTICLE';
...
export function addArticle(article) {
return {
type: ADD_ARTICLE,
article: article,
};
};
This is different than the getArticles function because we already made our API request. The only thing we are doing in this action is passing on the article object to the reducer.
Last step: add the new article to our the articles object in the Redux store.
// src/reducers/articlesReducer.js import { SET_ARTICLES, ADD_ARTICLE } from '../actions'; #1 const initialState = { articles: [] } export default function articlesReducer(state = initialState, action) { switch (action.type) { case SET_ARTICLES: return action.articles; case ADD_ARTICLE: #2 return [action.article, ...state]; default: return state; } }
Now try it out. In the browser you should be able to click on the Add Article link, fill out and submit the form, and be redirected to the articles page with the new article on top. Second action complete!
Our articles page lists all the articles from the API, and each article title has a link to an ArticleInfo page. Clicking it will result in another HTTP GET request to the API URL appended with the article id (e.g., localhost:3001/articles/3). We will also have a button to send an HTTP DELETE request. From a database CRUD prespective we will be using Read and Delete commands.
Start with the component:
// src/components/articles/ArticleInfo.js import React, { useEffect } from 'react'; #1 import { useSelector, useDispatch } from 'react-redux'; #2 import axios from 'axios'; #3 import { Link } from 'react-router-dom'; import { setArticle, removeArticle } from '../../actions'; #4 function ArticleInfo(props) { #5 const article = useSelector((state) => state.article) #6 const dispatch = useDispatch(); #7 useEffect(function() { #8 axios.get(`/api/articles/${props.match.params.<%= @mongoid %>id}`) .then(function(response) { dispatch(setArticle(response.data)); #9 }) .catch(function(error) { console.log('error', error); }); }, [dispatch, props]); #10 function handleDelete() { #11 axios.delete(`/api/articles/${article.<%= @mongoid %>id}`) .then(function() { dispatch(removeArticle(article.<%= @mongoid %>id)); props.history.push("/") }) .catch(function(error) { console.log('error', error) }); } return ( <div> <h2>{article.title}</h2> <small>id: {article.<%= @mongoid %>id}</small> <p>{article.content}</p> <div className="btn-group"> <Link to={{ pathname: `/articles/${article.<%= @mongoid %>id}/edit` }} className='btn btn-info'>Edit</Link> <button className="btn btn-danger" type="button" onClick={handleDelete}>Delete</button> <Link to="/" className="btn btn-secondary">Close</Link> </div> <hr/> </div> ) } export default ArticleInfo;
So our component dispatches two different actions, setArticle and removeArticle. Let's create those actions:
// src/actions/index.js ... export const SET_ARTICLES = 'SET_ARTICLES'; export const ADD_ARTICLE = 'ADD_ARTICLE'; export const SET_ARTICLE = 'SET_ARTICLE'; export const REMOVE_ARTICLE = 'REMOVE_ARTICLE'; ... export function setArticle(article) { return { type: SET_ARTICLE, article: article, }; }; export function removeArticle(<%= @mongoid %>id) { return { type: REMOVE_ARTICLE, <%= @mongoid %>id: <%= @mongoid %>id, }; };
Populate the articleReducer file. This is not the same file or reducer as articlesReducer. ArticleReducer manages state and actions for a single article object, while the articlesReducer manages state and actions for an array of article objects.
// src/reducers/articleReducer.js import { SET_ARTICLE } from '../actions'; export default function articleReducer(state = {}, action) { #1 switch (action.type) { case SET_ARTICLE: #2 return action.article; default: return state; } };
With the removeArticle action we want to remove the deleted article from the articles object in the Redux store. Another alternative would be to refresh articles object with another API query but that wouldn't be very efficient would it? No it wouldn't.
// src/reducers/articlesReducer.js import { RECEIVE_ARTICLES, ADD_ARTICLE, REMOVE_ARTICLE } from '../actions'; const initialState = { articles: [] } export default function articlesReducer(state = initialState, action) { switch (action.type) { case SET_ARTICLES: return action.articles; case ADD_ARTICLE: return [action.article, ...state]; case REMOVE_ARTICLE: return state.filter(article => article.<%= @mongoid %>id !== action.<%= @mongoid %>id); default: return state; } }
Now test it out. Create an article, click on its info page, then delete it. That's two more actions we've successfully programmed. On to the final piece of CRUD. Update.
Now let's add an edit form that makes an HTTP PATCH request to the server and Updates an existing article in the database.
// src/components/articles/ArticleEdit.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { patch } from 'axios';
import { setArticle, replaceArticle } from '../../actions';
function ArticleEdit(props) {
const initialState = useSelector((state) => state.article)
let [article, changeArticle] = useState(initialState)
const dispatch = useDispatch();
function handleChange(event) {
changeArticle({...article, [event.target.name]: event.target.value});
}
function handleSubmit(event) {
event.preventDefault();
if(!article.title || !article.content ) return
patch(`/api/articles/${article._id}`, {title: article.title, content: article.content})
.then(function(response) {
dispatch(setArticle(article));
dispatch(replaceArticle(article));
})
.then(function() {
props.history.push(`/articles/${article._id}`)
})
.catch(function(error) { console.log(error); });
};
function handleCancel() {
props.history.push(`/articles/${article._id}`);
}
return (
<div>
<h1>Edit {article.title}</h1>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Title</label>
<input type="text" name="title" defaultValue={article.title} onChange={handleChange} className="form-control" />
</div>
<div className="form-group">
<label>Content</label>
<textarea name="content" rows="5" defaultValue={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;
We won't go through the code this time since most of the concepts were covered in ArticleInfo and ArticleAdd.
Note we are getting the article state from the Redux store, not doing another API query. In a production app you would add some code to do an API call if the user refreshes the edit page which will clear out the store, but we'll keep it simple and not do that.
The only thing we need to do in our action and dispatch is to change the articles object by replacing the old version of the article with the new version.
Now let's finish the Actions page. This is the final code.
// src/actions/index.js
import { get } from 'axios';
export const SET_ARTICLES = 'SET_ARTICLES';
export const ADD_ARTICLE = 'ADD_ARTICLE';
export const SET_ARTICLE = 'SET_ARTICLE';
export const REMOVE_ARTICLE = 'REMOVE_ARTICLE';
export const REPLACE_ARTICLE = 'REPLACE_ARTICLE';
export function setArticles() {
return function(dispatch) {
return get('/api/articles')
.then(function(response) {
dispatch({type: SET_ARTICLES, articles: response.data})
})
.catch(function(error) { console.log('error', error); });
};
};
export function addArticle(article) {
return {
type: ADD_ARTICLE,
article: article,
};
};
export function setArticle(article) {
return {
type: SET_ARTICLE,
article: article,
};
};
export function removeArticle(_id) {
return {
type: REMOVE_ARTICLE,
_id: _id,
};
};
export function replaceArticle(article) {
return {
type: REPLACE_ARTICLE,
article: article,
};
}
And the final code for the articlesReducer.
// src/reducers/articlesReducer.js
import { SET_ARTICLES, ADD_ARTICLE, REMOVE_ARTICLE, REPLACE_ARTICLE } from '../actions';
const initialState = { articles: [] }
export default function articlesReducer(state = initialState, action) {
switch (action.type) {
case SET_ARTICLES:
return action.articles;
case ADD_ARTICLE:
return [action.article, ...state];
case REMOVE_ARTICLE:
return state.filter(article => article._id !== action._id);
case REPLACE_ARTICLE:
return state.map(function(article) {
if (article._id === action.article._id) {
return {
...article,
title: action.article.title,
content: action.article.content,
}
} else return article;
})
default:
return state;
}
}
Make sure all the files are saved, then try it out in the browser. Add an article, click to go to its info page, update it, then delete it. That's the last of our Components. One more optional step then we are done.
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.
Congratulations you now have a fully functioning React with Redux app using hooks that can perform all four CRUD operations on the database through the API. That's a lot. To put it in production one option is to use Heroku. To find out how to do that go to the optional last part of this MERN tutorial series Deploy a MERN app to Heroku