Redux is commonly used to implement the Flux architectural pattern in ReactJS applications.
The Redux implementation of Flux requires a store that is integrated with the database. We’ll do this in two phases.
Redux applications are based on a store, actions and reducers.
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.
Install the Redux library using the Node Package Manager (npm).
npm install redux --save
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.
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.
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.
Save summaries of your answers to these questions in a
lab14.txt
file in the root of your lab repo.
The Flux store for the comments application must be based on the existing Mongo database.
Add Flux-compatible MongoDB access to the current application as follows. This will require a few new actions/reducers to support asynchronous database access.
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.
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:
loadCommentsFromServer()
→
loading-comments reducer
handleCommentSubmit()
→
add-comment reducer
handleUpdate()
→
edit-comment reducer
The key differences are as follows:
setState()
have been
removed because the Redux store now takes care of
global application state.
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.
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.
Save summaries of your answers to these questions in your
lab14.txt
file.
You can compare your solution with the course repo.
We now integrate the Flux store created above with the running ReactJS UI.
We’ll now refactor the existing React UI by moving the database access code from the ReactJS components into the new Flux store.
Copy these utility functions into your flux.js
under
StoreTools:
storeTools.js
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.
We must now replace the hard-coded manipulation of the database in the CommentBox component with dispatch calls to the new store.
Replace the imports from global
and jQuery
with an import from flux.js
:
import { store, ActionTools } from './flux';
Remove the loadCommentsFromServer()
and the
componentDidMount()
functions. We’ve
moved these reponsibilities to the store.
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.
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.
The CommentEdit component will need similar updates.
Add imports of store, ActionTools,
StoreTools
from flux.js
.
import { store, ActionTools, StoreTools } from './flux';
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.
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.
Save summaries of your answers to these questions in your
lab14.txt
file.
The Heroku deployment should still work as before.
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.
We will grade your work according to the following criteria: