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.
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:
- To start off, a known, fixed state is retrieved
- Thereafter, a view is rendered, which makes it impossible to alter the state for this render sequence
- If the state is the same, view rendering will always be effected in a similar manner
- Event listeners gather data on user actions and network requests, and consequently send them to the Redux store
- 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
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:
Impure functions can cause side-effects or mutate state, which can cause the function to return inconsistent values:
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:
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:
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
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
Here is an example of an action:
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:
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.
What other bugs have you run across in Redux? Please let us know in the comments.