Enhancing your React + GraphQL app with Redux and Redux-Thunk

Vladimir Kopychev
Level Up Coding
Published in
10 min readDec 21, 2017

--

Continuing the React + GraphQL full stack tutorial, applying a Redux approach and Middlewares

Back to the past

In my previous story, we created a GraphQL API with a React JS single-page application (SPA) as a Frontend. For that tutorial we avoided some of the complexities of a real-world app to keep things as simple as possible. In that tutorial the user search form was blank every time on page load. To solve this problem we’ll use temporary storage to store search parameters.

We will also be saving user Authentication for a later story. It will not be considered in this story because this topic deserves an entirely separate article.

And, finally, we used the Container approach in the last tutorial, where some components were containers which managed their state within themselves, and also performed all side effects such as AJAX API calls. This approach works fine for relatively small applications, but when your application grows, keeping state and actions together inside components becomes less convenient.

It’s not always a straightforward problem finding out which fragment of state and which action is situated in which component. Also, JavaScript files containing code for components become bigger and less readable with time. To solve this problem we need to extract state from the containers and make global application state a ‘single source of truth.’ Actions for manipulations based on the state, and all side effects like API calls or storage usage, should also be abstracted from the component’s code.

To solve this problem I’d suggest using the popular and well-known state-management approach and library called Redux. You can find, clone, and launch working code in this new repository created for a Redux-enhanced application with GraphQL on the back-end.

Why Redux?

There are many tools for Data Flow and State management — so Why Redux?

  • Well-known state management container for JS apps with a big community and active support.
  • Works very well with React JS applications.
  • Allows you to have a global state store as a ‘single source of truth.’
  • Keeps state tree independent from UI and presentational components.
  • Certain slices of your state can be mapped to specific part of your app.
  • Provides flexibility in handling actions and side-effects (API calls etc.).

The last point above deserves special attention. According to Redux concepts, a reducer is a Pure function that manages a fragment of the state of your app (accepts an initial state and action as parameters and returns a new state).

const formReducer = (state = initialState, action) => {
switch (action.type) {
case 'FORM_LOAD':
return {
...state,
data: action.data,
}
default:
return state
}
}

So, no side-effects such as API calls or Storage manipulations should be used in reducers. But what to do if we need them? Like asynchronous load of users/todo lists? For this purpose the Middleware connect exists.

Middleware and asynchronous actions

Middlewares are extremely useful for async actions and API calls. You can use middleware to handle all pipelines from the beginning of the AJAX request till the moment data is retrieved. Let’s look at a small example — we need to ask a back-end API to provide us a list of users, but we want to put our app in the ‘Loading’ state when the API call is in progress and in the ‘Error’ state if we have any errors while dispatching data.

This pattern is called middleware. It takes your parameters and returns a new function which implements all side-effects needed and dispatches your actual actions, subsequently affecting your reducers and the state of your application. We are also able to use async/await patterns to handle Promises and AJAX calls.

With redux-thunk middleware it will look like.

const getUsers = (params) => async dispatch => {
try {
dispatch(requestUsers())
const data = await ApiService.getUsers(params)
dispatch(receiveUsers(data))
} catch(e) {
dispatch(failureUsers())
}
}

With the async action created, we will put our container in the Loading state by dispatching requestUsers() action. It will wait for data to be retrieved from the server and, after that, either update our state with a users list by calling receiveUsers() action, or put it into the Error state by dispatching failureUsers() action.

This middleware has more ‘superpowers,’ such as access to the current state of your application via the getState function. It will be shown later in this story.

All actions are just simple functions which trigger reducers.

const requestUsers = () => ({
type: 'USERS_REQUEST'
})
const receiveUsers = (data) => ({
type: 'USERS_RECEIVE',
data
})
const failureUsers = () => ({
type: 'USERS_FAILURE',
})

Rethinking containers

As we saw in the previous story, containers are ‘smart’ components which handle their own state and trigger actions and side-effects like API calls, storing the data received in the state and passing it down as props in child components.

For our refactored application we need to rethink containers in a Redux-way. Containers in Redux are `stateless` components which are connected to a global state store. Redux gives us the ability to map the slices of state we need and the actions we want to pass to the container’s props. Instead of state, the container’s props are updated. One of the core principles of Redux is to handle the whole application state via a store and reducers.

The following example is showing our TodosListContainer. We manage our state via reducers and map specific slices of state to our component’s props via mapStateToProps, and specific actions to props via mapDispatchToProps functions. We apply those function to our Container with the connect() function imported from ‘react-redux’ module.

We dispatch loadTodos on component mount to retrieve Todos list. We also have specific conditions to handle the Loading and Error flags that are coming from the bit of state related to the ‘Todo’ part.

import React from 'react'
import TodoList from '../components/TodoList'
import { Link } from 'react-router-dom'
import { connect } from 'react-redux'
import { todosAction } from '../actions/todosActions'
class TodoListContainer extends React.Component { componentDidMount() {
const userId = parseInt(this.props.match.params.userId, 10);
//load todos here
this.props.loadTodos(userId)
}
render() {//add loading and failure state
if (this.props.isLoading) {
return <span>Loading...</span>
}
if (this.props.isFailure) {
return <span>Error loading todos!</span>
}
return (
<div className="todo">
<TodoList todos={this.props.todos} />
<Link className="todo__linkback" to='/'>
Back to Users search
</Link>
</div>
);
}
}const mapStateToProps = ({ todos }) => {
return {
...todos
}
}
const mapDispatchToProps = (dispatch) => {
return {
loadTodos: (userId) => {
dispatch(todosAction({ userId }))
}
}
}
export default connect(
mapStateToProps, mapDispatchToProps
)(TodoListContainer);

Redux router

Redux router or React-router-redux is an enhanced react router which allows you to coordinate React router with your redux store, and to sync a piece of state related to routing with your store. It also provides you with a really useful push() function, which allows you to preform redirects in Redux actions in a functional way.

In this tutorial we will not use the main features of Redux router, but it’s good to have it for future improvements of our application (i.e. Authentication). If you don’t want to install and use it, it’s also fine to use the generic React-router for this tutorial.

Using storage for Search Form

To keep form data synced between different routes, we need some storage to both put form data into now, and retrieve it from later. For this tutorial we will choose localStorage. There are several reasons for this:

  • Local storage data doesn’t have a lifetime, it persists until we remove it.
  • Local storage doesn’t require any additional libraries or tools — everything can be done via HTML5 localStorage API.

To abstract from the actual storage engine — so we can switch to sessionStorage or cookies, and to follow better practices in general — we create a StorageService to work with localStorage data.

This class contains a reference to the actual storage engine and useful methods to get/set/remove values. We will call it in our redux middlewares to populate Search Form data.

We are going to fill this storage with data after successful search requests — it will be described later in this tutorial.

class StorageService {
constructor() {
this.storage = window.localStorage
this.form_key = 'search_form'
}
getSearchData() {
let data = this.storage.getItem(this.form_key) || false;
if (data) {
return JSON.parse(data)
}
}
setSearchData(data) {
if (data) {
this.storage.setItem(
this.form_key, JSON.stringify(data)
);
}
}
removeSearchData() {
this.storage.removeItem(this.form_key);
}
}export default new StorageService()

Handling forms with redux

In the previous tutorial we controlled the UserForm component, which had its own state, with all input changes mapped into it. To make this form work with a Redux-friendly approach we need to create a reducer and actions for it. There’s also the awesome Redux-Form library which gives you a lot of functionality ‘out-of-the-box,’ but it is probably part of a different story. :)

Let’s start with actions. We define actions to load form data from localStorage, to change Form data if one of the inputs has changed, and to clear Form data, because we will store successful search requests from here on out, so a method for clearing will be required.

import StorageService from '../StorageService'const loadForm = (data) => ({
type: 'FORM_LOAD',
data,
})
const clearForm = () => ({
type: 'FORM_CLEAR',
})
export const loadFormAction = () => (dispatch) => {
//load form data from local storage
const data = StorageService.getSearchData() || {}
//dispatch an action
dispatch(loadForm(data))
}
export const clearFormAction = () => (dispatch) => {
StorageService.removeSearchData()
dispatch(clearForm())
}

The reducer for Form will look pretty simple — just loadForm to load form data from the user input or localStorage, and clearForm to clear data.

const formReducer = (state = {}, action) => {
switch (action.type) {
case 'FORM_LOAD':
return {
...state,
...action.data,
}
case 'FORM_CLEAR':
return {}
default:
return state
}
}
export default formReducer

You can see how the UserForm component is rewritten and how handlers and props are passed there from the UserContainer component.

Wrapping up — Redux main concepts in our app

To install all the discussed dependencies, just run:

npm i -s redux react-redux redux-thunk
npm i -s react-router-redux@next //if you want to use redux-router

Or use can use the package.json file from the repository.

To use all our reducers with the global state, we will use the combineReducers function from Redux. It allows us to combine pieces of state from each reducer with the appropriate alias. See the index.js file in /reducers.

const reducer = combineReducers(
{
form: formReducer,
todos: todosReducer,
users: usersReducer,
routerReducer
}
)
export default reducer

After this our state object will look like:

state = {
form: {}, //form reducer part
users: {}, //users reducer part
todos: {}, //todos reducer part
routerReducer: {}, //react-router-redux part
}

Now let’s move onto main index.js file:

const history = createHistory()
const middleware = [thunk, routerMiddleware(history)]
const store = createStore(
reducer,
applyMiddleware(...middleware)
);
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
document.getElementById('root'));
registerServiceWorker();

The first two lines create all middlewares needed — thunk for middlewares, and async actions and routerMiddleware to handle history and routing.

After that, we just init the store with our combined reducer mentioned before, and wrap our application in Provider with store passed into it.

Let’s see how the Redux container works using the UsersListContainer as an example:

import React from 'react'
import UserList from '../components/UserList'
import UserForm from '../forms/UserForm'
import { connect } from 'react-redux'
import { getUsers } from '../actions/usersActions'
import { loadForm, loadFormAction, clearFormAction } from '../actions/formActions'
class UserListContainer extends React.Component { componentDidMount() {
this.props.loadForm();
this.props.search();
}
render() {
//add loading and failure state
if (this.props.isLoading) {
return <span>Loading...</span>
}
if (this.props.isFailure) {
return <span>Error loading users!</span>
}
return <div className="user">
<UserForm data={this.props.form}
submitHandler={this.props.search}
changeHandler={this.props.changeForm}
clearHandler={this.props.clearForm} />
{this.props.users &&
<UserList users={this.props.users} />}
</div>;
}
}
const mapStateToProps = ({ users, form }) => {
return {
...users,
form,
}
}
const mapDispatchToProps = (dispatch) => {
return {
search: () => { dispatch(getUsers()) },
loadForm: () => { dispatch(loadFormAction()) },
changeForm: (params) => { dispatch(loadForm(params)) },
clearForm: (params) => {
dispatch(clearFormAction())
dispatch(getUsers())
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(UserListContainer);

In this container we map ‘users’ and ‘form’ sections of our state to the container’s props. We also map all actions we need via mapDispatchToProps function. When the component mounts we load form data from storage, and perform a user search with the current parameters. We also pass some of the actions to UserForm as handlers.

The Users Reducer manages the user-related section of our state and handles Request/Receive/Failure action types:

const initialState = {
isLoading: false,
isFailure: false,
users: null,
}
const usersReducer = (state = initialState, action) => {
switch (action.type) {
case 'USERS_REQUEST':
return {
...state,
isLoading: true,
isFailure: false,
}
case 'USERS_RECEIVE':
return {
...state,
isLoading: false,
isFailure: false,
users: action.data,
}
case 'USERS_FAILURE':
return {
...state,
isLoading: false,
isFailure: true,
}
default:
return state
}
}
export default usersReducer

The User Actions file contains one middleware function which dispatches all those actions, performing an async API call and storing form data upon successful requests. Note that in this tutorial we use getState functionality in the middleware function. This function provides you with access to your current state, so you can grab, for example, form data or other bits of state without having to pass them to the action.

import ApiService from '../ApiService'
import StorageService from '../StorageService'
const requestUsers = () => ({
type: 'USERS_REQUEST',
})
const receiveUsers = (data) => ({
type: 'USERS_RECEIVE',
data,
})
const failureUsers = () => ({
type: 'USERS_FAILURE',
})
export const getUsers = (params) => async (dispatch, getState) => {
try {
dispatch(requestUsers())
const params = getState().form
const data = await ApiService.getUsers(params)
if (Object.keys(params).length && data) {
//save successful request
StorageService.setSearchData(params)
}
dispatch(receiveUsers(data))
} catch (e) {
dispatch(failureUsers())
}
}

As a result, we now have our SPA, with state being fully managed by Redux. We have two ‘smart’ containers (TodosList and UsersList) and we left presentational components unchanged (aside from a bit of refactoring in UserForm component, which is not only for presentational purposes). Form data is now saved between route changes: for example, we can search for a user, look at todos, then go back to the list and it will still be filtered! Feel free to clone the repo and test the code :)

In my next stories, we will talk about handling authentication in GraphQL as well is in React-Redux, and about unit testing React-Redux apps.

--

--

Full Stack Engineer with passion for Frontend and UI solutions, PhD, coding at @udemy