Redux is commonly used to implement the Flux architectural pattern in ReactJS applications.

Using the Flux Store

The Redux implementation of Flux requires a store that is integrated with the database. We’ll do this in two phases.

Redux Implementation

Redux applications are based on a store, actions and reducers.

Exercise 14.1

Make a copy of your solution for the previous lab and store it under lab14. Now, refactor the application to use a Flux-compatible store as follows.

  1. Install the Redux library using the Node Package Manager (npm).

    npm install redux --save
  2. Create a new module in the application scripts directory for the necessary Flux features based on this template:

    flux-template.js

    You’ll fill out this template as you go through the lab. For the moment, rename it to flux.js and make sure that you understand the features of this code and how they relate to the key elements of Redux applications.

  3. Redux actions are implemented as declarative action specifications. Implement the necessary action-building utilities in ActionTools using the following template.

    let ActionTools = {
        addComment: function(comment) {
        return {
            type: 'ADD_COMMENT',
            comment: comment
            };
        },
        editComment: function(id, comment) {
            return {
                type: 'EDIT_COMMENT',
                id: id,
                comment: comment
            };
        }
    }

    These functions will receive a comment object (with values for author, title and, potentially, id) and create an appropriate action spec.

  4. The UI components will dispatch action requests to the store. In Redux, the store encapsulates a few components.

    • Dispatcher — The Redux dispatcher uses a utility method, called commentsApp in our code, that receives action requests and calls the appropriate reducer. Here is the dispatcher entry for the ADD_COMMENT and EDIT_COMMENT requests.

      function commentsApp(state, action) {
          switch (action.type) {
              case 'ADD_COMMENT':
                  Reducers.addComment(action);
                  return state;
              case 'EDIT_COMMENT':
                  Reducers.editComment(action);
                  return state;
              default:
                  return state;
          }
      }

      Note that for our database-driven application, this dispatcher utility leaves the current state of the store unchanged; we’ll deal with the state updates below.

    • Reducers — The dispatcher calls the appropriate reducer function. Reducers modify the store’s state and, in our application, update the database. Because these reducer functions interact with the Mongo database, we’ll deal with them in the next exercise. You may need to create reducer stubs here to get the code to run.

    • State — The Redux store maintains the (one and only) application state. For the comments application, this state matches the one currently maintained by CommentBox.

      let defaultState = {
          data: []
      };

      The default state is a(n empty) list of comment objects.

With an understanding of this Redux-based tooling, add the Redux-based tooling that you’ll need to implement a delete-comment function based on the functionality of your previous solution.

The application should still run as it did before; we haven’t changed any running code. When you’ve confirmed this, consider the following.

  1. Does the comments application, as it it was implemented in the previous lab, implement a Flux pattern? If so, how? If not, why not?
  2. Consider how we could use these new components to refactor the comments application to use Redux.

Save summaries of your answers to these questions in a lab14.txt file in the root of your lab repo.

Database Integration

The Flux store for the comments application must be based on the existing Mongo database.

Exercise 14.2

Add Flux-compatible MongoDB access to the current application as follows. This will require a few new actions/reducers to support asynchronous database access.

  1. We need two new actions, LOADING_COMMENTS and LOADED_COMMENTS. Add these to ActionTools.

    let ActionTools = {
        loadingComments: function() {
            return {
                type: 'LOADING_COMMENTS'
            };
        },
        loadedComments: function(comments) {
            return {
                type: 'LOADED_COMMENTS',
                comments: comments
            };
        },
        
    }

    These actions correspond to the initial (asynchronous) request for data from the database and the eventual return of the data from that request respectively.

  2. We’ll also need the update the dispatcher utility and add new reducers to handle these new actions.

    • Dispatcher

      function commentsApp(state, action) {
          switch (action.type) {
              case 'LOADING_COMMENTS':
                  Reducers.loadingComments();
                  return state;
              case 'LOADED_COMMENTS':
                  return { data: action.comments };
              
          }
      }

      Note that LOADED_COMMENTS is the only dispatcher entry that returns a modified state, which is produces by inserting the comment list passed with the action parameter into the new, returned state. We’ll see where this comment list comes from later.

    • Reducers — The dispatcher calls the appropriate reducer function. Reducers modify the store’s state and, in our application, interact with the database. Copy the reducers implemented here into your flux.js under Reducers:

      reducers-template.js

      The AJAX code for these reducers is mostly copied from the ReactJS components we were using before:

      • CommentBox:
        • loadCommentsFromServer() → loading-comments reducer
        • handleCommentSubmit() → add-comment reducer
      • CommentEdit:
        • handleUpdate() → edit-comment reducer

      The key differences are as follows:

      • The calls to setState() have been removed because the Redux store now takes care of global application state.
      • On successful completion of the (asynchronous) LOADING_COMMENTS action, we dispatch a LOADED_COMMENTS action to handle the new comment data.
      • There is no reducer for the LOADING_COMMENTS action because that is implemented directly in the dispatcher utility function (see above). Make sure that you know what it does and how it works.
      • Edit comment now gets the ID of the comment being edited from the action (rather than from the ReactJS prop value): API_URL + "/" + action.id.

With an understanding of this Redux/AJAX-based tooling, add the Redux-based reducer that you’ll need to implement a delete-comment function.

The application should still run as it did before; we still haven’t changed any running code. When you’ve confirmed this, consider the following.

  1. Review the new Redux mechanism and consider how the ReactJS components will use it to handle asynchronous communication with the database.
  2. What benefit, if any, is there is moving the state processing and AJAX database access logic out of the ReactJS components and into the Redux store?

Save summaries of your answers to these questions in your lab14.txt file.

You can compare your solution with the course repo.

Integrating the Flux Store with the React UI

We now integrate the Flux store created above with the running ReactJS UI.

Exercise 14.3

We’ll now refactor the existing React UI by moving the database access code from the ReactJS components into the new Flux store.

  1. Copy these utility functions into your flux.js under StoreTools:

    storeTools.js
  2. Initialize the store when the application starts by adding this code to index.js.

    
    import { StoreTools } from './flux';
    StoreTools.startLoadingComments();
    
    ReactDOM.render(
        
    );

    This code uses the startLoadingComments() utility function to start requesting periodic updates from the database, which used to be done in the CommentBox component.

  3. We must now replace the hard-coded manipulation of the database in the CommentBox component with dispatch calls to the new store.

    1. Replace the imports from global and jQuery with an import from flux.js:

      import { store, ActionTools } from './flux'; 
    2. Remove the loadCommentsFromServer() and the componentDidMount() functions. We’ve moved these reponsibilities to the store.

    3. Modify handleCommentSubmit() by replacing the AJAX call with a dispatch call to the store.

      handleCommentSubmit: function(comment) {
          var comments = this.state.data;
          comment.id = Date.now();
          var newComments = comments.concat([comment]);
          this.setState({data: newComments});
          store.dispatch(ActionTools.addComment(comment));
      },

      Note that the code continues to optimistically set the UI values, but ultimate source of truth is now the store.

    4. Add the following code to register the CommentBox component to receive store update messages.

      componentWillMount() {
          this.unsubscribe = store.subscribe(() => {
              this.setState({
                  data: store.getState().data
              });
          });
      },
      componentWillUnmount: function() {
          this.unsubscribe();
      },

      The first function subscribes to the store’s update messages and specifies that the component’s state should be updated with the store data (this.setState(…)). The second function unsubscribes when the component is not being used, i.e., when it is unmounted.

  4. The CommentEdit component will need similar updates.

    1. Add imports of store, ActionTools, StoreTools from flux.js.

      import { store, ActionTools, StoreTools } from './flux';
    2. Remove the loadData() function; we’ll get data from the store by modifying componentDidMount() as follows.

      componentDidMount: function() {
          let commentToEdit = StoreTools.findComment(this.props.params.id, store.getState().data);
          this.setState({author: commentToEdit.author, text: commentToEdit.text});
      },

      This code gets the full state from the store, uses a utility function to find the comment currently being edited and pushes the values out to the UI component state.

    3. Modify handleUpdate() as follows.

      handleUpdate: function() {
          var updatedComment = {
              author: this.state.author.trim(),
              text: this.state.text.trim()
          };
          store.dispatch(ActionTools.editComment(Number(this.props.params.id), updatedComment));
          this.context.router.push('/');
      },

      Here, we dispatch an EDIT_COMMENT action with the comment ID and new values based on the data in the HTML form. As with the CommentBox component, there is no need to connect directly to the database using AJAX; that code has been moved to the reducers above. Note that we did move the redirect to the root path (/) out of AJAX and into this handler; we’re letting the store reducer work asynchronously.

Make similar modifications, as appropriate, to handleDelete() to support the delete operation configured above.

The application has been refactored significantly, but refactored code should still run in the same manner as the original. When you’ve confirmed this, consider the following.

  1. Can you explain how the data flows from the database to the UI? How do new comments and edits get pushed to the database?
  2. What advantages, if any, does the current code have over the original? Disadvantages?

Save summaries of your answers to these questions in your lab14.txt file.

The Heroku deployment should still work as before.

Exercise 14.4

Verify that the application deploys properly to Heroku as it did before. Do the redeploy as you did in the previous lab, but call this branch production14. Remember to include the URL of your Heroku deployment in a README.md file in your lab 14 solution directory.

Checking in

We will grade your work according to the following criteria: