Introduction

In this tutorial we will create a React with Redux and React Router application that interacts with a database to do Create-Read-Update-Delete (CRUD) transactions through an API. That sounds like a lot. And it is. But we'll try to make it as straightforward as possible. We'll use a Ruby on Rails API-only application for our backend, although you can build your own with another framework if you prefer.

This tutorial assumes you are familiar with React, JavaScript ES6 syntax, and the concepts involved with API calls/asynchronous HTTP requests. It assumes only limited knowledge of Redux. You may find the World's Simplest Redux with APIs Example to be a good introduction.


Build an API Back End

For a CRUD application we need to build an API on the server to interact with the database. This tutorial's main focus is on the React and Redux front end. So you can use any framework you like for building the back end. You just need an endpoint at localhost:3001/articles to receive GET requests for all articles and POST requests to create new ones. And an endpoint at localhost:3001/articles/:id to receive GET, PATCH and DELETE requests for specific articles. And a database table called "articles" with fields for id, title, and content.

If you have a Ruby on Rails environment set up on your computer, we have a whole separate Rails API Tutorial. If you are already familiar with Rails, we'll quickly create an API that meets our requirements right now. Here are the steps starting from your Terminal:

  • Generate a new Rails API-only application:
  • mkdir articles-api; cd articles-api
  • If you are using RVM for Ruby and Gems version control you can set your gemset at this point.
  • rails new . --api
  • Generate an Articles scaffold which will create your database table, model, controller, and routes:
  • rails generate scaffold Article title:string content:text
  • rails db:migrate
  • Change the index action in the Articles controller to order them most recent first:
  • # app/controllers/articles_controller.rb 
    ...
    def index
      @articles = Article.all.order(created_at: :desc)
      render json: @articles
    end
    
  • Add some seed data. Something like the below:
  • # db/seed.rb
    Article.create(title: "Learn React", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
    Article.create(title: "Learn APIs", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
    Article.create(title: "Learn Redux", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
    
  • rails db:seed
  • Add and configure the rack-cors gem to allow access from your React app.
  • # Gemfile
    gem 'rack-cors', require: 'rack/cors'
    
  • bundle install
  • # config/initializers/cors.rb
    Rails.application.config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*',
          headers: :any,
          methods: [:get, :post, :put, :patch, :delete, :options, :head]
      end
    end
    

    Add an "api" namespace to the routes. This will avoid potential route collisions between your back-end api and your front-end react routes.

    # config/routes.rb
    scope '/api' do
      resources :articles
    end
    
  • Run the server and test it out.
  • rails server -p 3001
  • View the JSON data at http://localhost:3001/api/articles.json
  • Your API is ready to go.


    Set up a React app with Redux

    Use Create React App to set up a React app.

  • create-react-app redux-crud-app
  • cd redux-crud-app

    Packages:

    Install your packages:

  • npm install --save redux react-redux redux-thunk redux-logger react-router-dom axios
  • or

    yarn add redux react-redux redux-thunk redux-logger react-router-dom axios

    Run the server to make sure it's working:

    npm start or yarn start

    You should see the default Welcome to React app in your browser.


    Set up the File Structure

    Use the below Unix commands to create directories and remove, move, and create files.

    rm src/logo.svg
    rm src/App.test.js
    rm src/registerServiceWorker.js
    touch src/history.js
    mkdir src/stylesheets
    mv src/index.css src/stylesheets/index.css
    mv src/App.css src/stylesheets/App.css
    mkdir src/components
    mv src/App.js src/components/App.jsx
    touch src/components/ArticleList.jsx
    touch src/components/ArticleInfo.jsx
    touch src/components/ArticleAdd.jsx
    touch src/components/ArticleEdit.jsx
    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'll cover these 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.

    The components folder holds our React components. Redux recommends splitting all your React components into Presentational Components, which hold the JSX, and Container components, which interact with Redux. We're not going to do that because our component files will be pretty slim as it is, and it's easier to understand the flow of the code if we don't. In a real-world app with lots of stuff in each component it would make sense to split them that way.

    For convenience we'll use the Bootstrap CDN for our styling. Import it at the top of the index.css file.

    // src/stylesheets/index.css
    
    @import url('https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css');
    

    Our app is broken now but we'll get it working again shortly.


    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 './stylesheets/index.css';
    import App from './components/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. It can work independently of Redux. Quoting from Redux docs:
    "Redux will be the source of truth for your data and React Router will be the source of truth for your URL. In most of the cases, it is fine to have them separate unless you need to time travel and rewind actions that trigger a URL change."

    Our navigation will be contained in the App component. We will set up a nav bar with two menu items, articles and new article, and add all our routes up front.

    // src/components/App.jsx
    
    import React, { Component } from 'react';
    import ArticleAdd from './ArticleAdd';
    import ArticleList from './ArticleList';
    import ArticleInfo from './ArticleInfo';
    import ArticleEdit from './ArticleEdit';
    import {Router, Route, NavLink, Switch} from 'react-router-dom'
    import history from '../history';
    
    class App extends Component {
      render() {
        return (
          <Router history={history}>
            <div className="container">
              <Navigation />
              <Main />
            </div>
          </Router>
        );
      }
    }
    
    const Navigation = () => (
      <nav className="navbar navbar-expand-lg navbar-dark bg-dark">
        <ul className="navbar-nav mr-auto">
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles">Articles</NavLink></li>
          <li className="nav-item"><NavLink exact className="nav-link" activeClassName="active" to="/articles/new">Add Article</NavLink></li>
        </ul>
      </nav>
    );
    
    const Main = () => (
      <Switch>
        <Route exact path="/" component={ArticleList} />
        <Route exact path="/articles" component={ArticleList} />
        <Route exact path="/articles/new" component={ArticleAdd} />
        <Route exact path="/articles/:id" component={ArticleInfo} />
        <Route exact path="/articles/:id/edit" component={ArticleEdit} />
      </Switch>
    );
    
    export default App;
    

    We won't cover how React Router works in this tutorial. The docs are at reacttraining.com/react-router/core/guides.

    There is one thing different than a React without Redux app. We are importing from a file called "history" and adding it as an attribute to the Router element: <Router history={history}>. This is because we will be using functions outside of our components to interact with Redux. Per React-Router's docs, this requires accessing React's build-in history module. Populate the history file with these two lines:

    // src/history.js
    
    import { createBrowserHistory } from 'history';
    
    export default createBrowserHistory();
    

    To test if our initial Redux app and router are working let's add placeholder components to our component files.

    // src/components/ArticleList.jsx
    
    import React from 'react';
    export default function Placeholder() {
      return (<h1>Articles Page</h1>);
    }
    
    // src/components/ArticleAdd.jsx
    
    import React from 'react';
    export default function Placeholder() {
      return (<h1>Add Article</h1>);
    }
    
    // src/components/ArticleInfo.jsx
    
    import React from 'react';
    export default function Placeholder() {
      return (<h1>Article Info</h1>);
    }
    
    // src/components/ArticleEdit.jsx
    
    import React from 'react';
    export default function Placeholder() {
      return (<h1>Edit Article</h1>);
    }
    

    Restart the server: npm start or yarn start

    Now when you go to localhost:3000 you should see a navbar with two working routes. 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 './stylesheets/index.css';
    import App from './components/App';
    import rootReducer from './reducers';
    import { getArticles } from './actions';                                 #1
    
    const store = createStore(rootReducer, applyMiddleware(thunk, logger));
    
    store.dispatch(getArticles());                                           #2
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>, 
      document.getElementById('root')
    );
    

    1. Import the getArticles 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 getArticles 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 getArticles 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 axios from 'axios';                                           #1
    
    export const RECEIVE_ARTICLES = 'GET_ARTICLES';                      #2
    
    const apiUrl = 'http://localhost:3001/api/articles';                     #3
    
    export const getArticles = () => {                                   #4
      return (dispatch) => {
        return axios.get(`${apiUrl}.json`)                               #5
          .then(response => {
            dispatch({type: RECEIVE_ARTICLES, articles: response.data})  #6
          })
          .catch(error => { throw(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. For convenience we'll create a variable for our API URL.
    4. GetArticles() will make our API call and use the dispatch method to send an action to the reducer.

    5. Make an HTTP GET request to our API endpoint using AJAX via the Axios module.
    6. 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 RECEIVE_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.

    Rather than defining our Action object directly in the dispatch method, we could create a separate Action Creation Function and call it from dispatch() like the below. We aren't going to do that in this tutorial, but be aware that's a common way to do it. And of course you can break all the action functions out this way if you like.

    const receiveArticles = (data) => ({
      type: RECEIVE_ARTICLES,
      articles: data,
    });
    export const getArticles = () => {
      return (dispatch) => {
        return axios.get(`${apiUrl}.json`)
          .then(response => {
            dispatch(receiveArticles(response.data))
          })
          .catch(error => { throw(error); });
      };
    };
    

    And if we wanted to add a "Loading..." spinner between when the API request is made and when the response is received, we could add another action called REQUEST_ARTICLES, and change the User Interface to show a spinner. But we'll keep it simple and skip that.

    Now that we've sent our retrieved articles to the reducer with our action, let's define the reducer.

    // src/reducers/articlesReducer.js
    
    import { RECEIVE_ARTICLES } from '../actions';                           #1
    
    const initialState = { articles: [] }
    export default function articlesReducer(state = initialState, action) {  #2
      switch (action.type) {
        case RECEIVE_ARTICLES:                                               #3
          return action.articles;
        default:                                                             #4
          return state;
      }
    }
    
    1. Import the RECEIVE_ARTICLES string constant we declared in the actions file.
    2. Declare our reducer function with two arguments, state and action. Use ES6 default parameter syntax to set the initial state to an empty articles array.
    3. Use a switch statement to match the action type. If the action type is RECEIVE_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. Replace our placeholder component with this:

    // src/components/ArticleList.jsx
    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    import { Link } from 'react-router-dom';
    
    class ArticleList extends Component {
      render() {                                                        #1
        if(this.props.articles.length) {                                #2
          return (
            <div>
              <h4>Articles</h4>
              {this.props.articles.map(article => {                     #3
                return (
                  <div key={ article.id }>                              #4
                    <hr/>          
                    <h4><Link to={`/articles/${article.id}`}>{article.id}: {article.title}</Link></h4>  #5
                    <p>{article.content}</p>
                  </div>
                );
              })}
            </div>
          )    
        } else {
          return (<div>No Articles</div>)
        }
      }
    }
    
    const mapStateToProps = (state) => ({ articles: state.articles });  #6
    
    export default connect(mapStateToProps)(ArticleList);               #7
    
    1. The ArticleList component's render method returns the JSX that will be converted by React to HTML.
    2. Use a conditional to make sure there are articles (i.e., articles.length is truthy).
    3. Use the JavaScript map method to return each article in the format specified in the function.
    4. React requires a unique key to iterate over an array, so we'll use the article id.
    5. 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).
    6. Notice in the render method that we are using this.props to access the articles. How did the articles array get into the component's props? With React-Redux's mapStateToProps method, that's how. This method you accesses the store's current state and maps it to your props according to your specifications. Here we are mapping the articles props to the articles array from the store.
    7. React-Redux's Connect method links Redux store to the React component. It takes the mappings from the mapStateToProps method and adds them to the props of the component listed just after it, ArticleList in this case.

    Now if you entered all the above code, and your API server is running on port 3001, you should be able to go to your browser and see your list of articles. Our first action is a success!


    Post Articles - Connect the ArticleAdd Component

    We have good momentum after our first successful Redux Action. Time for our second. This time our 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. Replace the placeholder component with this new shiny one.

    // src/components/ArticleAdd.jsx
    
    import React from 'react';
    import { connect } from 'react-redux';
    import { addArticle } from '../actions';                      #1
    
    class ArticleAdd extends React.Component {
      state = { title: '', content: '' };
    
      handleChange = (event) => {
        this.setState({ [event.target.name]: event.target.value });
      };
    
      handleSubmit = (event) => {
        event.preventDefault();
        this.props.addArticle(this.state);                        #4        
      };
    
      render() {
        return (
          <div>
            <h4>Add Article</h4>
            <form onSubmit={ this.handleSubmit }>
              <div className="form-group">
                <input type="text" name="title" required value={this.state.title} onChange={this.handleChange} 
                  className="form-control" placeholder="Title" />
              </div>
              <div className="form-group">
                <textarea name="content" rows="5" required value={this.state.content} onChange={this.handleChange} 
                  className="form-control" placeholder="Content" />
              </div>
              <button type="submit" className="btn btn-dark">Create</button>
            </form>
          </div>
        );
      }
    }
    
    const mapDispatchToProps = { addArticle };                     #2
    
    export default connect(null, mapDispatchToProps)(ArticleAdd);  #3
    

    Most of this is similar to plain React. Set initial state to empty values. Create a form with fields that use the onChange event and a handle change handler function to update the fields as you type. And have an onSubmit event with a handle submit handler function. Then React-Redux takes over.

    1. Import the addArticle function from the Actions file. It will handle our POST request and our Action.
    2. Down at the bottom of the file is a React-Redux statement similar to the mapStateToProps function we defined in the ArticleList component. mapDisptachToProps is an object that holds all the action creation functions that will be called from our component. In this case there is just one, addArticle.
    3. React-Redux's connect function adds the functions from mapDispatchToProps to the ArticleAdd component's props. Since we don't have a mapStateToProps function, set the first argument to null.
    4. Now in our handleSubmit function, we can call the addArticle function and pass it the component's state with the form data.

    Time to define our addArticle function. We'll add it to the actions folder.

    // src/actions/index.js
    
    import axios from 'axios';
    import history from '../history';
    
    export const RECEIVE_ARTICLES = 'GET_ARTICLES';
    export const ADD_ARTICLE = 'ADD_ARTICLE';
    
    const apiUrl = 'http://localhost:3001/api/articles';
    
    ...
    
    export const addArticle = ({ title, content }) => {                                                      #1
      return (dispatch) => {
        return axios.post(`${apiUrl}.json`, {title, content})                                                #2
          .then(response => {
            let data = response.data;
            dispatch({type: ADD_ARTICLE, payload: {id: data.id, title: data.title, content: data.content}})  #3
          })
          .then(() => {
            history.push("/articles")                                                                        #4
          })
          .catch(error => { throw(error) });
      };
    };
    

    This is similar to the getArticles function. Only this time it is a post request so we are sending data.

    1. When the addArticle function is called, the form data is sent as the argument. It is an object with title and content properties.
    2. We use Axios to send an AJAX POST request to the API endpoint including the data object.
    3. If it is posted successfully, the API's server will return the new article data as a JSON string that Axios converts to a JavaScript object. Here we use the Redux dispatch method to send the action to the reducer, with the data in a payload property.
    4. Then we use React-Router and the history method we imported at the top of the file to redirect back to the /articles route.

    Last step is to add the action to the articlesReducer.

    // src/reducers/articlesReducer.js
    
    import { RECEIVE_ARTICLES, ADD_ARTICLE } from '../actions';              #1
    
    const initialState = { articles: [] }
    export default function articlesReducer(state = initialState, action) {
      switch (action.type) {
        case RECEIVE_ARTICLES:
          return action.articles;
        case ADD_ARTICLE:                                                    #2
          return [action.payload, ...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/ArticleInfo.jsx
    
    import React, { Component } from 'react';
    import { connect } from 'react-redux';
    import { Link } from 'react-router-dom';
    import { getArticle, deleteArticle } from '../actions';
    
    class ArticleInfo extends Component {
      componentDidMount() {                                                         #4
        this.props.getArticle(this.props.match.params.id);
      }
    
      render() {
        const article = this.props.article;
        return (
          <div>
            <h2>{article.id}: {article.title}</h2>
            <p>{article.content}</p>
            <div className="btn-group">
              <Link to={{ pathname: `/articles/${article.id}/edit`, state: { article: article } }} className='btn btn-info'>  #5
                Edit
              </Link>
              <button className="btn btn-danger" type="button" onClick={() => this.props.deleteArticle(article.id)}>          #6
                Delete
              </button>
              <Link to="/articles" className="btn btn-secondary">Close</Link>                                                 #7
            </div>
            <hr/>
          </div>
        )
      }
    }
    
    const mapStateToProps = (state) => ({ article: state.article });                 #1
    
    const mapDispatchToProps = { getArticle, deleteArticle };                        #2
    
    export default connect(mapStateToProps, mapDispatchToProps)(ArticleInfo);        #3
    

    We'll go in the order that the statements are executed:

    1. React-Redux's mapStateToProps method is the first to execute. There is initially no article loaded so there is nothing to map at first.
    2. MapDispatchToProps maps the getArticle and deleteArticle functions imported from the actions file to the component's props.
    3. React-Redux's connect method adds the mapped state and Dispatch functions to the ArticleInfo component's props.
    4. componentDidMount is a built-in React function called once the component is mounted. This is where we call the getArticle method that will fetch the article data from the API. We are passing the article id as the argument, which uses React-Router's match method to get from the route parameter (e.g., articles/3 would be id:3).
    5. React-Router provides the Link component which we'll use to add a link to the Edit page.
    6. We are also adding a Delete button with an onClick event. Here we are defining the handler function directly in the element. It calls the deleteArticle function.
    7. Use React-Router's Link component to link back to the Articles page.

    Add the getArticle and deleteArticle functions to the actions folder:

    // src/actions/index.js
    
    import axios from 'axios';
    import history from '../history';
    
    export const RECEIVE_ARTICLES = 'GET_ARTICLES';
    export const ADD_ARTICLE = 'ADD_ARTICLE';
    export const RECEIVE_ARTICLE = 'RECEIVE_ARTICLE';
    export const REMOVE_ARTICLE = 'REMOVE_ARTICLE';
    
    const apiUrl = 'http://localhost:3001/api/articles';
    ...
    export const getArticle = (id) => {
      return (dispatch) => {
        return axios.get(`${apiUrl}/${id}.json`)
          .then(response => {
            dispatch({type: RECEIVE_ARTICLE, article: response.data});
          })
          .catch(error => { 
            throw(error); 
          });
      };
    };
    
    export const deleteArticle = (id) => {
      return (dispatch) => {
        return axios.delete(`${apiUrl}/${id}.json`)
          .then(response => {
            dispatch({type: REMOVE_ARTICLE, payload: {id}})
          })
          .then(() => {
            history.push("/articles")
          })
          .catch(error => {
            throw(error);
          });
      };
    };
    

    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 { RECEIVE_ARTICLE } from '../actions';
    
    export default function articleReducer(state = {}, action) {  #1
      switch (action.type) {
        case RECEIVE_ARTICLE:                                     #2
          return action.article;
        default:
          return state;
      }
    };
    
    1. Set the initial state to an empty object.
    2. If the action type is RECEIVE_ARTICLE then the reducer returns the article data to be added to the store.

    The deleteArticle function deletes the article from the database and redirects back to the Articles page. But we want the deleted article to be removed from the articles list. We could refresh the api query but that wouldn't be very efficient. Instead we use the articlesReducer to remove it from the articles array in the store.

    // 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 RECEIVE_ARTICLES:
          return action.articles;
        case ADD_ARTICLE:
          return [action.payload, ...state];
        case REMOVE_ARTICLE:
          return state.filter(article => article.id !== action.payload.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/ArticleEdit.jsx 
    
    import React from 'react';
    import { connect } from 'react-redux';
    import { updateArticle } from '../actions';
    
    class ArticleEdit extends React.Component {
      handleChange = (event) => {
        this.setState({ [event.target.name]: event.target.value });
      };
    
      handleSubmit = (event) => {                                                             #1
        event.preventDefault();
        const id = this.props.article.id;
        const title = this.state.title ? this.state.title : this.props.article.title;
        const content = this.state.content ? this.state.content : this.props.article.content;
        const article = {id: id, title: title, content: content}
        this.props.updateArticle(article);
      };
    
      handleCancel = () => {
        this.props.history.push(`/articles/${this.props.article.id}`);
      }
    
      render() {
        return (
          <div>
            <h1>Edit {this.props.article.title}</h1>
            <form onSubmit={this.handleSubmit}>
              <div className="form-group">
                <label>Title</label>
                <input type="text" name="title" defaultValue={this.props.article.title} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="form-group">
                <label>Content</label>
                <textarea name="content" rows="5" defaultValue={this.props.article.content} onChange={this.handleChange} className="form-control" />
              </div>
              <div className="btn-group">
                <button type="submit" className="btn btn-dark">Update</button>
                <button type="button" onClick={this.handleCancel} className="btn btn-secondary">Cancel</button>
              </div>
            </form>
          </div>
        );
      }
    }
    
    const mapStateToProps = (state) => ({ article: state.article });
    
    const mapDispatchToProps = { updateArticle };
    
    export default connect(mapStateToProps, mapDispatchToProps)(ArticleEdit);
    

    We won't go line by line this time since most of the concepts were covered in ArticleInfo and ArticleAdd.

    1. In the handleSubmit handler function we are setting the id, title, and content properties. That's because if the user doesn't change a field it will be sent as undefined. So we use the ternary statement to set it to the original props value if the state value is undefined.

    The original state is still the article object from ArticleInfo. We are not doing a database refresh pull since we already have the info in the store. 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.

    Now let's finish the Actions page. This is the final code.

    // src/actions/index.js
    
    import axios from 'axios';
    import history from '../history';
    
    export const RECEIVE_ARTICLES = 'GET_ARTICLES';
    export const ADD_ARTICLE = 'ADD_ARTICLE';
    export const RECEIVE_ARTICLE = 'RECEIVE_ARTICLE';
    export const REMOVE_ARTICLE = 'REMOVE_ARTICLE';
    export const UPDATE_ARTICLE = 'UPDATE_ARTICLE';
    export const REPLACE_ARTICLE = 'REPLACE_ARTICLE';
    
    const apiUrl = 'http://localhost:3001/api/articles';
    
    export const getArticles = () => {
      return (dispatch) => {
        return axios.get(`${apiUrl}.json`)
          .then(response => {
            dispatch({type: RECEIVE_ARTICLES, articles: response.data})
          })
          .catch(error => { throw(error); });
      };
    };
    
    export const addArticle = ({ title, content }) => {
      return (dispatch) => {
        return axios.post(`${apiUrl}.json`, {title, content})
          .then(response => {
            let data = response.data;
            dispatch({type: ADD_ARTICLE, payload: {id: data.id, title: data.title, content: data.content}})
          })
          .then(() => {
            history.push("/articles")
          })
          .catch(error => { throw(error) });
      };
    };
    
    export const getArticle = (id) => {
      return (dispatch) => {
        return axios.get(`${apiUrl}/${id}.json`)
          .then(response => {
            dispatch({type: RECEIVE_ARTICLE, article: response.data});
          })
          .catch(error => { 
            throw(error); 
          });
      };
    };
    
    export const deleteArticle = (id) => {
      return (dispatch) => {
        return axios.delete(`${apiUrl}/${id}.json`)
          .then(response => {
            dispatch({type: REMOVE_ARTICLE, payload: {id}})
          })
          .then(() => {
            history.push("/articles")
          })
          .catch(error => {
            throw(error);
          });
      };
    };
    
    export const updateArticle = (article) => {                                                                #1
      const articleId = article.id;
      return (dispatch) => {
        return axios.patch(`${apiUrl}/${article.id}.json`, {title: article.title, content: article.content})
          .then(response => {
            const data = response.data;
            dispatch({type: UPDATE_ARTICLE, payload: {id: data.id, title: data.title, content: data.content}})
            dispatch({type: REPLACE_ARTICLE, payload: {id: data.id, title: data.title, content: data.content}})
          })
          .then(() => {
            history.push(`/articles/${articleId}`)
          })
          .catch(error => { throw(error) });
      };
    };
    
    1. The updateArticle function uses Axios to send an AJAX PATCH request to the API. If successful, the server sends a response with the updated article object. Then we dispatch not one but two actions.

    The dispatch with action type UPDATE_ARTICLE goes to the articleReducer to change the article object in the store.

    // src/actions/articleReducer.js
    
    import { RECEIVE_ARTICLE, UPDATE_ARTICLE } from '../actions';
    
    export default function articleReducer(state = {}, action) {
      switch (action.type) {
        case RECEIVE_ARTICLE:
          return action.article;
        case UPDATE_ARTICLE:
          return {
            id: action.id,
            title: action.payload.title,
            content: action.payload.content,
          }
        default:
          return state;
      }
    };
    

    The dispatch with action type REPLACE_ARTICLE goes to the articlesReducer to replace the appropriate article object in the articles array.

    // src/actions/articlesReducer.js
    
    import { RECEIVE_ARTICLES, ADD_ARTICLE, REMOVE_ARTICLE, REPLACE_ARTICLE } from '../actions';
    
    const initialState = { articles: [] }
    export default function articlesReducer(state = initialState, action) {
      switch (action.type) {
        case RECEIVE_ARTICLES:
          return action.articles;
        case ADD_ARTICLE:
          return [action.payload, ...state];
        case REMOVE_ARTICLE:
          return state.filter(article => article.id !== action.payload.id);
        case REPLACE_ARTICLE:
          return state.map((article) => {
            if (article.id === action.payload.id) {
              return {
                ...article,
                title: action.payload.title,
                content: action.payload.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. Congratulations you now have a fully functioning React with Redux and React Router app that can perform all four CRUD operations on the database through the API. Whew.