Introduction

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.


Set up a React app with 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",
        ...
    }
    

    Set up the File Structure

    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.


    Set up Redux

    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')
    );
    
    1. In addition to the standard React setup, we are setting up our Redux store with the createStore method. It takes our root reducer as the first argument, imported from the src/reducers/index.js file. And we will be applying the Thunk middleware package for our asynchronous API calls, and Logger for logging our actions in the Console to help with development.
    2. The Redux-React package enables Redux to interact with React. Wrapping our main App element in Redux-React's Provider component with a store attribute makes the Redux store available to all our React components.

    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,
    });
    

    Set up React Router

    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.


    Get Articles - Connect the ArticleList Component

    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')
    );
    

    1. Import the setArticles method from the actions folder. It doesn't exist yet but we'll define it next.
    2. We are applying the dispatch method directly to the store object. It is calling the setArticles method which will do an API call then use an action to get the data in the store. This is the only time we will apply the dispatch method directly on the store. The rest of the actions will be dispatched from our components.

    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.

    1. Import the Axios module to facilitate making AJAX calls to our API.
    2. Redux recommends using string constants for our Action types rather than string literals, so we'll declare it here and use it below and later in our reducer function.
    3. SetArticles() will make our API call and use the dispatch method to send an action to the reducer.
    4. Make an HTTP GET request to our API endpoint using AJAX via the Axios module. We don't need to use the full URL, just the path. We added the domain portion as a proxy in the client/package.json file.
    5. Using ES6 Promises if we get a successful response we will call the dispatch method and send an Action. In this case the action type is SET_ARTICLES, and we are sending the API response data with the action as a payload called "articles." Then the reducer will add it to the store.

    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;
      }
    }
    
    1. Import the SET_ARTICLES string constant we declared in the actions file.
    2. Declare our reducer function with two arguments, state and action. Set the initial state to an empty articles array.
    3. Use a switch statement to match the action type. If the action type is SET_ARTICLES it returns the articles data to update the store with.
    4. You need a default case. If there is no match, the reducer will just return the current 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;
    
    1. Import the useSelector hook from the react-redux package. React-redux has two main hooks that we will be using in our app... useSelector and useDispatch. You can read about them at react-redux.js.org/next/api/hooks. If you are familiar with Redux without hooks, useSelector replaces the the mapStateToProps method.
    2. Create the ArticlesList component as a function. React and React-Redux hooks use functional components instead of class components.
    3. Employ the useSelector hook to get the articles array from the Redux store and add it to our component's state. Assign it to a variable we'll call "articles". The articles state is an object that contains an array of the articles we got from the API. We set up our app to do the API query and add the results to the redux store when we first open the app.
    4. Our component returns the list of articles in JSX which React converts to HTML.
    5. We assigned the articles object we pulled from the Redux store to the variable articles and put it in our component's state. So now we can use the JavaScript map method to iterate over the articles array to present is as we specify in the map function. However, React will run the return statement twice. Once before we get our articles from the store and once after. This will cause the map method to throw an error the first time since there is no articles array. To avoid that we make it a conditional, checking that there are articles first with articles.length. If that evaluates to false then it won't go on to the articles.map method.
    6. React requires a unique key to iterate over an array, so we'll use the article id.
    7. Use React-Router's Link component to add a hyperlink to each article title that will take the user to its ArticleInfo component (to be defined later).

    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.


    Post Articles - Connect the ArticleAdd Component

    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;
    
    1. We haven't used the React hooks yet, of which there are two main ones... useState and useEffect. Import useState which will allow us to add state to a functional component.
    2. React-redux also has two main hooks. We used useSelector hook in the ArticleList component for accessing existing state. Now we'll use the useDispatch hook to modify the Redux store.
    3. Import the addArticle function from the Actions file.
    4. Create our component as a function.
    5. useState: 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 "article" to the current state value, and "setFields" to the update function.
    6. React-Redux's useDispatch hook returns a reference to the dispatch function from the Redux store. We use it to dispatch our addArticle action below. This replaces the mapDispatchToProps method you would use in a class component without hooks.
    7. 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 setFields 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.
    8. When the user presses the submit button it calls the handleSubmit function. This is where our API post request is sent with the article object sent as the payload. If it successfully posts it will send back the new article object. Then we dispatch the addArticle action passing in the new article object.

    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;
      }
    }
    
    1. Import ADD_ARTICLE from the actions folder.
    2. Add the action type to the switch statement. It adds the new article to the beginning of the existing articles array from the store's current 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!


    Get Article and Delete Article - Connect the ArticleInfo Component

    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;
    
    1. React has two main hooks. We already saw the useState hook in the ArticleAdd component which sets the component's state. This time we'll import the other main hook, useEffect.
    2. Likewise, react-redux has two main hooks. We used one of each in ArticleList and ArticleAdd. This time we'll import and use both of them.
    3. We used the Axios libary in the last to actions to make get and post requests to the API. This time, instead of importing the specific actions we are going to use (get and delete), we are importing the whole Axios library. 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.
    4. Import the Redux actions we will be using.
    5. Declare our component as a function. Pass in props as a parameter because we will be accessing the article id in the component from the params prop.
    6. Call the react-redux useSelector hook to get the article object from the Redux store and assign it to the component's state. This replaces the mapStateToProps method you would use in a class component without hooks.
    7. React-redux's useDispatch hook returns a reference to the dispatch function from the Redux store. We use it to dispatch our setArticle and removeArticle actions below. UseDispatch replaces the mapDispatchToProps method you would use in a class component without hooks.
    8. useEffect: The React useEffect hook essentially replaces the lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount). Use it when we need to rerender our component. Making an API call in the component is one such cases since we will need to change the state in our component after we get back the response. Code inside the useEffect method runs after the initial render of the component.
    9. Dispatch the setArticle action, passing the article object as the argument.
    10. The useEffect hook takes a second argument called the dependency array that instructs React to rerun the effect only when the listed elements in the array change. React will post a warning in the console if you leave something out.
    11. In our return statement holding our JSX=>HTML there is a delete button with the onClick property calling the handleDelete function. HandleDelete sends the delete request to the API then dispatches the removeArticle action passing in the article id.

    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;
      }
    };
    
    1. Set the initial state to an empty object.
    2. If the action type is SET_ARTICLE then the reducer returns the article data to be added to the store.

    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.


    Update Article - Connect the ArticleEdit Component

    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.


    Run the API and React Client with one command

    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
  • Open the package.json file in the project's root directory (not the one in the client folder) and add the highlighted script below. Note that you need to add a comma after the start script.

    // 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