Redux is an amazing JavaScript library that has become mainstream because of its simplicity, tiny 2kb size, and excellent documentation. Redux assists developers to manage state in JavaScript applications and ensure it performs consistently, regardless of the environment.

However, despite Redux’s awesomeness and versatility, software will always have bugs. It is the responsibility of the careful developer to minimize them and their impact on the users. We’ve gathered five common sources of bugs using Redux, and some ideas to prevent them.

1. Non-determinism

Determinism, or repeatability, ensures an application generates the same output when provided with the same input, regardless of the number of times it runs. An application with a non-deterministic state reproduction increases the occurrence of bugs, and complicates the debugging process.

Redux was developed to encourage determinism in development by supporting the separation of state management from I/O side effects. For example, rendering the DOM and outputting network tasks. However, some developers have not mastered this concept and fail to implement it in their Redux code, leading to “non-determinism” bugs.

This problem can be fixed by separating the innately non-deterministic aspects, such as user input actions and network I/O, from deterministic aspects, such as state transition and event processing. This way, the Redux code is much less complicated to debug than when everything is conglomerated with DOM updates and network parameters.

Separating DOM rendering from network requests and state updates, you can effectively implement determinism in your application development. Consequently, you can get rid of various problems, including race situations arising from asynchronous aspects haphazardly obliterating bits of your view before rendering is complete.

Redux imposes an obligatory isolation and ordering technique, which follows the following procedure—at all times:

  1. To start off, a known, fixed state is retrieved
  2. Thereafter, a view is rendered, which makes it impossible to alter the state for this render sequence
  3. If the state is the same, view rendering will always be effected in a similar manner
  4. Event listeners gather data on user actions and network requests, and consequently send them to the Redux store
  5. Once the information is sent, the state is updated to a new recognized state, and the loop continues. Only sent actions are capable of reaching the state.

2. Impure Reducer Functions

Reducer functions are key building blocks for successfully implementing Redux state management. Redux reducers (derived from the array reduce method in JavaScript) are functions that take the present state of the app and an action, and then return the desired new state.

Without correctly implementing reducers in your code, your application can be problematic. Importantly, to realize deterministic state reproduction in your application development, you must ensure that reducers functions are pure functions—at all times.

A function is pure if:

  • If provided with the same input values, it will return the same output values
  • Does not generate side effects
  • Does not depend on any external mutable state

Here is an example of pure functions:

// a pure function
function square(z) {
  return z * z;
}

// a pure function, which depends on another pure function
function squareAll(numbers) {
  return numbers.map(square);
}

Impure functions can cause side-effects or mutate state, which can cause the function to return inconsistent values:

// impure function that mutates state.
function square(z) {
  square.lastNumber = z;
  return z * z;
}

// impure function that causes a side-effect
function squareAll(numbers) {
  for (var i = 0; i < numbers.length; i++) {
    numbers[i] = square(numbers[i]);
  }
}

Pure function reducers return new state objects, rather than mutating the previous state, something that increases predictability and reduces programming headaches.

To reduce bugs in your code, begin with a single reducer function, and as your application increases in size, divide it into smaller reducers that handle various states of your application. When used properly, reducers allow you to control their calling sequence, pass extra data, and even develop reusable reducers that can conveniently be used anywhere in your app.

Here’s an example of a reducer function that adds one or more users to the current state:

import state from 'state'

const initialState = state([])

function exampleReducer(state = initialState, action) {
  if (action.type === 'USERS_ADD') {
    return state.concat(action.payload);
  }

  return state;
}

store.dispatch({
  type: 'USERS_ADD',
  payload: user
});

3. Multiple Sources of Truth

In Redux, the state of your entire application needs to be stored in a “single source of truth”; that is, the storage is done in a single place. Therefore, if the state needs to be accessed from any place within the application, you need to reference its single source of truth.

If you fail to obey this principle, you can end up with stale data or some instances of shared state mutation bugs that Redux aims to overcome. It can be tempting to store small bits of state wherever it is convenient for the component-stuffing it in the DOM, or creating a new state object, but it can be costly later.

Here’s an example of multiple sources of truth:

var state = store.getState();
var el = document.querySelector('.user');

// implied state in the DOM based on the classes on elements
if (state.showUser && el.hasClass('visible')) {
  store.dispatch({
    type: 'USER_HIDE',
    payload: user
  });
}

With the single source of truth principle, you will achieve several things, including:

  • Ease in developing universal applications
  • Ease in debugging or inspecting an app
  • Faster app development cycle
  • Deterministic view rendering
  • Deterministic state reproduction

4. Generic Actions

In Redux, an action refers to a plain JavaScript object that sends data to the store with the intention of changing its state. Importantly, to avoid bugs in your Redux code, you should ensure your actions are as small as possible.

You should only include the minimal amount of information necessary to change the state of the app to the desired one—and nothing more! This is inline with the generic Single Responsibility Principle in software design.

Furthermore, you should always use specific names for action types. This way, it’s much simpler to trace them to the reducer function that utilizes them from the action history, and conveniently make any desired improvements. Although more generic names are tempting, it can be more difficult to debug what is actually happening.

For example, if you use the generic FORM_CHANGE as an action name, scouring your application to understand what’s taking place is difficult. Which form was it? How did it change? Instead, you can significantly reduce your headaches if you use descriptive names such as LOGIN_FORM_SUBMIT.

Here is an example of an action:

store.dispatch({
  type: LOGIN_FORM_SUBMIT,
  payload: {
  username: 'jane', password: '76832' }
});

5. Coupled Actions

Actions are a core concept in redux, and the cause of state changes. If they are embedded with your views or reducers, it can be tricky to debug how they came to be.

In Redux, action creators refer to functions that create actions—just like the name suggests. To maximize their functionality and reduce bugs, you should ensure they are decoupled from your views and reducers. You should only use them for dispatching actions, rather than amassing and emanating them directly from your views.

Here is an example of an action creator:

function trackUser(form) {
  return {
    type: LOGIN_FORM_SUBMIT,
    payload: form
  };
}

If you write several JavaScript objects that relay information on mutations that should take place, it can make your code repetitive and prone to errors. However, with action creators, you can eliminate repetitive code in your application, and ensure all actions are conveniently kept in one location.

Using action creators properly can help you decrease boilerplate in your code. For example, if you keep your constants, reducers, and action creators in the same place, you can decrease boilerplate needed for importing them from various sites.

Wrapping Up

Redux is an amazing JavaScript library for web application state management, especially if you can fix the common bugs associated with using it. Although Redux is commonly used with React, it can also be used with other libraries, such as AngularJS, Backbone, and Vue.js.

Of course, TrackJS integrates with Redux and all other JavaScript libraries so you can easily find and fix the bugs that sneak into your code. Grab a free 30-day trial today!

What other bugs have you run across in Redux? Please let us know in the comments.