This image is a nice representation of how modern web-applications work. I found it on this article about Web API’s on a Raspberry Pi
In the last year we, at Greenhouse Group, have developed several React-based applications.
One of the latest projects I’m working on is an internal tool to monitor marketing expenses for the marketeers within our company. It was my responsibility to set up this new tool based upon the experiences we’ve gained and from a proof of concept the team had built.
I would like to share with you the story on how we created this application. What tools did we use and where we diverted from the traditional paths.
To give you an idea of the tools we use, here’s a list of what’s in our day-to-day toolkit:
We use Babel 6 to transpile our ES6 written code to be production-ready. Webpack is running as our dev-server and is the engine for building and compiling our code to the acceptance and production environments. For linting we rely on ESLint (The AirBnB preset with some customisations) and we use Jest for testing our code.
When we initially started we knew we had to implement some sort of state-management where we could store data application-wide rather than just per component to be more persistent while the user switched between views. From the moment when we started React development, we embraced Redux and we use it ever since. Although it might have it flaws and is not as extensive as Mobx for example, it serves our purpose and until today has not given us any implementation problems.
When you’re creating a web-application just having a static frontend isn’t enough. We need the data from our API’s and in Javascript the traditional way to get that is by XMLHttpRequests. Since we are building internal applications (and one of them is even an Electron app) we don’t have to worry much about browser coverage. We are only required to support Google Chrome, so even before we implemented API calls with XMLHttpRequest we decided to give that new Fetch API a try.
Then the next questions arise, how do you make API requests with React? The most common answer was in that time (beginning 2016) to do them in ComponentDidMount
. The response you got from the request would be stored in the Component’s local state and you are able to render the fetched data.
But with data in 1 component, how do you get the same data to a sibling?
Should we move the request to the parent? But what if we would sync that “component-state” with Redux and make it available in the application-wide state?
An option was to move the request to the state’s actions. But actions may not be blocking, while a Fetch request is. So the next step was to figure out Redux-Thunk. With Redux-Thunk you’ll get a middleware which reacts on Redux actions, does it things and then dispatches other actions again.
We fiddled with thunks for a while, but we ended up with quite some side-effects which gave testing problems and weren’t so easy to read. With the thunks you would dispatch an action, which then did its work and dispatched another action. It might be that our approach was a bit off or that it just hadn’t matured enought, but we were looking for a way where it was easier to follow what the side-effects were of our actions within the state.
It was October 2016 when we met Redux-Saga, quite fast we figured it could solve our problems. And after some concepts and less then a month later we integrated our first Saga in one of our applications. The main difference between Thunks and Saga’s is that where Thunks are controlled based upon the action that is dispatched, Saga’s listen independently to actions, more like reducers and act upon that.
In Saga’s it’s less complex to ask for a certain task and depending on the payload and results of certain steps in a Saga results will kick off.
I want to give you an example of how we make API requests in our code, it might clear up this complex matter;
Most of our Components are wrapped with Redux’ connect, which we call Connectors. The most simple Connector may look like this:
import { connect } from 'react-redux';import App from 'components/app';export default connect()(App);
Once a Component is mounted we would like to fetch data so it can be shown within the render of our Component.
This is one of those components:
import React from 'react';export default class App extends Component { componentDidMount() { this.props.fetchRecords(); } render() { const { records } = this.props; return ( <div className="app"> {records.map(record => ( <div className="record" key={record.id}>{record.name}</div> ))} </div> ); }}
(To keep my code blocks small, I didn’t add PropTypes in the samples)
As you can see, I used 2 props that weren’t included in the intial Connector. So I have to edit that file. I’ll add a dispatchToProps to the Redux connect and a stateToProps. The first one will, once called, dispatch an action to the store. The stateToProps will pass data directly from the records reducers to the Component.
In my example I’ll load the data directly from state, however internally we make use of Selectors (often in combination with Reselect) for easier re-use of state selectors and to optimize rendering once the reference of reducers changed, but the data itself hasn’t.
import { connect } from 'react-redux';import App from 'components/app';import fetchRecords from 'actions/fetch-records';const stateToProps = state => ({ records: state.records.data});const dispatchToProps = { fetchRecords};export default connect()(App);
Now, once the Component is mounted it will call fetchRecords
, which is due to the Redux Connect a function which will dispatch the action fetchRecords
to the store.
I have to add the following code to add a Saga which listens for fetchRecords
. Since I will not explain Redux-Saga in details in this article, Assume that I have a rootSaga which will spawn
my newly created Saga.
import { takeLatest, put, call } from 'redux-saga/effects';import fetchFailed from 'actions/fetch-failed';import setRecords from 'actions/set-records';export default function* onFetchRecords() { yield takeLatest('RECORDS/FETCH', function fetchRecords() { try { const response = yield call(fetch, 'https://api.service.com/endpoint'); const responseBody = response.json(); } catch (e) { yield put(fetchFailed(e)); return; } yield put(setRecords(responseBody.records)); });}
In the example above you can see that it will listen for RECORDS/FETCH
, once that action comes by it will do a Fetch request. When the Fetch fails it will throw an exception. In that case we will dispatch the fetchFailed
action. When the request succeeds it will dispatch setRecords
and will pass the records we got from the server.
Here you can see how our Reducer can handle those actions together with our Saga;
export default (state = {data: [], loading: false}, action = {}) => { switch (action.type) { case 'RECORDS/FETCH': case 'RECORDS/FETCH_FAILED': return { ...state, loading: true, data: [] }; case 'RECORDS/SET': return { ...state, loading: false, data: action.payload }; default: return state; }};
With these small pieces of code I’ve showed you a initial example of how you can move your API calls from ComponentDidMount to Redux Saga’s and have your data easily available in your Reducers in stead of only Component state.
Interested in more about this topic or does this sound cooler than you do in your day to day job? Let me know!