Writing Money Transfers in React — Part 5
In parts 1 to 4 we created a React JS application for money transfers between accounts, allowing the user to pick to and from and how much. Initially we just created a plain list, then added some behaviour and finally used Redux to manage state so that changes worked.
This is all well and good, but we’ve had to hard code the initial transfer list and when you refresh the page you are back were you started.
In this final part we look at how to handle async requests to get and save data when using Redux, and also look at the strategy I had to take so that the calls to the server don’t affect the front-end performance or overload the server.
The code for this can be found here on Github
Node API
Up until now Node has just been used to serve static assets, now we are going to use node to serve up a list of transfers and accept changes to them.
Saving and updating to a store or database is outside the scope of this tutorial. For the purposes of this walk through we’ll create a few API endpoints using a popular REST convention. The endpoints will be:
- GET /transfers Retrieve a list of transfers in JSON
- POST /transfers Add a new transfer.
- PUT /transfers/:id Update an existing transfer.
- DELETE /transfers/:id Delete an existing transfer.
In node I’ve created a controller that handles these actions in ‘server/controllers/transfers_controller.js’ and in a separate file I define the routes to the controller methods. You could do this all in one file but having them as separate modules allows you to unit test your controller code, very important.
Getting data from the server
In old school jquery you could use the ajax function and pass it callbacks to functions that would take the data and do stuff to the DOM. Redux is a little different, it has actions and reducers. In Redux you make your ajax request in an action creator, but the action creator is supposed to just return a object with a type and payload containing data. How do you cleanly create an action that returns the result of an asynchronous action?
Middleware
I’ve always hated the term ‘middleware’, it used to be an excuse for consultancies and professional service companies to make a fortune with non-descript products or code, thankfully this is not the case here. In Redux, middleware sits between the action creator and the reducer. A middleware component is passed the action that was created and can opt to do something with it or just pass it on to the reducers. With regards to the need wait for the result of an asynchronous action, a middleware component can use the idea of JavaScript promises. The component waits for the promise to be fulfilled and then passes that to the reducers.
React Promise
There are a few different middleware packages that support the idea of promises, the most popular being ‘react-promise’. The action creator creates an action with a type and a promise, instead of a payload. Once the promise has been full-filed the module passes the result to the reducer in the payload.
For many situations this is enough and will do the job, but I want something that allows my interface to know it’s doing a network call so the user is aware.
React Thunk
An alternative approach is to use ‘react-thunk’. This allows an action to return a function, and in that function you can dispatch one or more actions and have more flexibility over what you can do. This solution would allow you to do your ajax call and wait for the promise and then dispatch an action with the result. Again, for many people this is the best choice for them, it’s the most flexible but for me I feel I would Leade to code repetition and feel the logic belongs in one central place.
Roll your own
Finally I came across a discussion involving the guy who started Redux and it talked about a different approach and demonstrated how you can create your own middleware. The need to carry out an ajax call and keep the user informed is a common pattern so creating something to handle that is extremely useful. I took the example given and ran with it to make a version that is more flexible. With the middleware created you can create actions that look like this:
{
types: [SAVE_TRANSFER, UPDATED_TRANSFER, SAVE_TRANSFER_ERROR],
promise: transferUpdateRequest,
payload: transfer
}
This will result in an initial action being sent to the results that looks like:
{
type: SAVE_TRANSFER,
payload: transfer
}
Then if the ajax request works a 2nd call to the reducers will look like this:
{
type: UPDATED_TRANSFER,
payload: ... results of ajax call ...
}
I made the middleware flexible so if you just call it with 2 types it assumes the first is the success type and the 2nd is the error type. If you send it just one type, or the key is called ‘type’ instead then it just calls the reducer with a good result. I think this little piece of code is extremely flexible, re-usable and plan to put it on Github and NPM.
To get middleware to work you have to plumb it into the store:
import { createStore, applyMiddleware } from 'redux';
import promise from './middleware/promise_middleware';
const createStoreWithMiddleware = applyMiddleware(
promise
)(createStore);
const store = createStoreWithMiddleware(reducers)
Boilerplate again, but the idea is you wrap the create store factory in another factory that includes the middleware.
OK Lets GET something already!
Right with all that said, lets get the transfers. Remove the hard coded transfers data from the transfers action and replace the function to get transfers with this:
import axios from 'axios';export function getTransfers(reportId) {
return {
types: [GET_TRANSFERS, GET_TRANSFERS_ERROR],
promise: axios.get('/transfers')
};
}
I’m using the simpler version of my middleware as I don’t need to tell the user I’m doing something, and you may have also noticed ‘axios’. Axios is a very lightweight utility for making ajax requests that supports promises.
Over at the reducer there are a couple of things to change because the structure of the payload will now be a little different and it’s up to the reducer to add a new blank transfer if needed.
function updateTransfers(state, transfers) {
let newState = transfers;
if (!containsIncompleteTransfer(newState) || newState.length === 0) {
appendNewTransfer(newState);
}
return newState;
}case GET_TRANSFERS:
if (action.payload.hasOwnProperty('data')
&& action.payload.data.hasOwnProperty('transfers')) {
return updateTransfers(state, action.payload.data.transfers);
}
break;
Axios normally puts the response in ‘payload.data’, but assume nothing. We check that the response has data and the key we are interested in and uses that as the new state.
So now when you load the page it will initially show an empty list while it goes to the server, the server will respond with data and that will cause a new state and a re-render.
Smart saving
Right now when you change something it updates the state and renders that change to the screen. If you modified the action to save this data to the server it would indeed send it but it would do so for every mouse click or keypress on a transfer, that may overload the server on a big site as well as a few other things. In addition to simply saving you want to be aware of when the request is made and when a response is received.
With this in mind it seemed sensible to keep the job of saving data separate from updating the interface, and be triggered at different times. This means that a few new action types need to be created:
- SAVE_TRANSFER
- ADDED_TRANSFER
- SAVED_TRANSFER
- SAVE_TRANSFER _ERROR
The way it works is we create an action creator to save a transfer and call that either when an account selection changes or the user moves off the amount field. The save action creator then looks at the transfer and decides if this is an update or a new transfer.
export function saveTransfer(transfer) {
if (completeTransfer(transfer)) {
if (transfer.id === null) {
const request = axios.post('/transfers', { transfer });
return {
types: [SAVE_TRANSFER, ADDED_TRANSFER, SAVE_TRANSFER_ERROR],
payload: transfer,
promise: request
};
} const request = axios.put(`/transfers/${transfer.id}`,{transfer});
return {
types: [SAVE_TRANSFER, SAVED_TRANSFER, SAVE_TRANSFER_ERROR],
payload: transfer,
promise: request
};
}
}
If this is a new transfer the request is sent in a HTTP POST and the action fired with the response is a ADDEDTRANSFER action, otherwise a HTTP PUT is used and fires a SAVEDTRANSFER when done. Using different actions for these 2 situations is very important, when you add a transfer you want to use the id the server generates and assign it back to this transfer. Now the ADDED_TRANSFER handler looks for the transfer id in the data that was returned by the server and allocates it to the newly added transfer by looking for it using a temporary ID that has been allocated to it.
function updateNewWithRealId(state, transfer) {
let clonedState = state.slice(0);
for (let pos = 0; pos < clonedState.length; pos += 1) {
if (clonedState[pos].temporaryId == transfer.temporaryId) {
clonedState[pos].id = transfer.id;
clonedState[pos].temporaryId = null;
break;
}
}
return clonedState;
}...case SAVE_TRANSFER: {
let transfer = action.payload;
if (transfer.id === null) {
transfer.waitingForId = true;
}
return update(state, transfer);
}
case ADDED_TRANSFER: {
if (action.payload.hasOwnProperty('data')
&& action.payload.data.hasOwnProperty('transfer')) {
return updateNewWithRealId(state, action.payload.data.transfer);
}
break;
}
Finally the MoneyTransfer component needs to make use of the action, first include it into MoneyTransfers and pass it to MoneyTransfer, then in MoneyTransfer update the handlers for account to and from and setup a new function to handle onBlur on the amount field
setAccountTo = (account) => {
let newTransfer = this.props.transfer;
newTransfer.accountTo = account;
this.props.updateTransfer(newTransfer); if (completeTransfer(newTransfer)) {
this.props.saveTransfer(newTransfer);
}
}blurAmount = (event) => {
let newTransfer = this.props.transfer;
newTransfer.amount = event.target.value;
this.props.updateTransfer(newTransfer); if (completeTransfer(newTransfer)) {
this.props.saveTransfer(newTransfer);
}
}...<input type="text"
id="balance"
name="account[balance]"
className="form-control form-control__number"
value={transfer.amount}
onChange={this.setAmount}
onBlur={this.blurAmount}
/>
Now when you make changes they are sent to the server and saved. You can make changes, refresh the page and it will show the same transfers.
Letting the user know
Right now the whole saving process is invisible to the user, they don’t know if the page saved or not which can lead to confusion, and the user asking why they don’t have a save button. Lets create a simple indicator on screen.
The position of the save indicator may not be within the transfer list, you may want it at the top of the page or ever in s static element so it’s always visible. The solution will be to create a 2nd top level ReactJS component and link it to the same store. In the index.html add a placeholder for the indicator:
<div class="grid-row">
<header id="page-header" class="column-two-thirds">
<h1 class="heading-xlarge">Money transfers</h1>
</header>
<div class="column-one-third">
<span id="saving-indicator"></span>
</div>
</div>
Now we need to follow the usual pattern of creating:
- Action
- Reducer
- Component
We have the actions already so we create a reducer just to store saving state, saving-reducer.js
import { SAVE_TRANSFER, ADDED_TRANSFER, SAVED_TRANSFER } from '../actions/transfers_actions';export default function(state = 'NOTHING', action) {
switch (action.type) {
case SAVE_TRANSFER:
return SAVE_TRANSFER;
case SAVED_TRANSFER:
case ADDED_TRANSFER:
return SAVED_TRANSFER;
}
return state;
}
Now a component that just puts this on the screen
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { SAVE_TRANSFER, SAVED_TRANSFER } from '../actions/transfers_actions';const status = {
'NOTHING': { label: '', state: ''},
SAVE_TRANSFER: {label: 'Saving...', state: 'saving'},
SAVED_TRANSFER: {label: 'Saved', state: 'saved'}
};class Saving extends Component {
render() {
return (
<span id="save-status" data-status="{ status[this.props.saving].state }">
{ status[this.props.saving].label }
</span>
);
}
}Saving.propTypes = {
saving: React.PropTypes.string,
};function mapStateToProps({ saving }) {
return { saving };
}export default connect(mapStateToProps)(Saving);
And the last step, add it to your app:
ReactDOM.render(
<Provider store={store}>
<Saving />
</Provider>,
document.querySelector('#saving-indicator'));
And there you have it, when you save it flashes up messages to let you know what is going on. Once you have the framework in place and the basics covered, adding behaviour and markup is very quick and easy.
Wrap up
Adding saving looks easy, but coming up with a method to do this actually took some thought. Having 2 items on the page with no id and understanding changes from the server being separate from local changes was initially complex and buggy. Hopefully this final model is easy to manage. If you are lucky enough to work with a backend store that has GUID’s that you can generate anywhere then that would make life easier.
The final part of this walk through will sum up what I learned while creating this screen, and mention some of the extra pieces that went into the final screen but didn’t make it into this demo project.