Maximizing Debuggability with Redux

Written by edelstein | Published 2017/06/28
Tech Story Tags: react | web-development | redux | logrocket | debuggability

TLDRvia the TL;DR App

In my last blog post, Redux Logging in Production, I discussed one of the most important benefits of using Redux — debuggability. By using front end logging tools like LogRocket, developers can easily understand and fix tricky bugs in production by reviewing the actions and state changes leading up to a bug.

While this information is immediately useful in any Redux app, there is a lot more we can achieve by architecting an app with logging in mind. In this post I’m going to look at a few libraries and abstractions that make Redux logs even more useful by putting as much application data through Redux as possible.

Data Fetching

Fetching/sending data over the network is one of the most bug-prone parts of any app. Issues can arise from connectivity, unexpected data or incorrect logic. And things get extra complicated with polling, retry logic, optimistic mutations, etc.

Libraries like apollo-client for GraphQL, and redux-query for REST both facilitate fetching data from the network via Redux. They use Redux as a persistence layer, meaning that when debugging issues, you can inspect your Redux logs to see what data these clients have fetched and what the status is of in-flight requests.

Lets take a look at the Redux logs generated by redux-query:

Here we see the REQUEST_START action, which corresponds with a query being initialized. Looking at the action payload, we see all the information in the request, making it easy to debug. Once a response is received, redux-query emits a REQUEST_SUCCESS action with all the information about the response.

Logging requests and responses is only part of the magic of redux-query. Digging into the store we see a key queries which is where redux-query persists its internal state.

Above, we see objects for each of the queries the app carried out (one to reddit.com/r/frontend.json and one to reddit.com/r/reactjs.json). When debugging issues, we can dig into this state object to see information on in-flight requests, queryCount (if we’re polling on a query), and timings.

Storing this information in Redux is critical, since it puts full context on all network activity in the Redux logs.

Rolling your own data fetching “framework”

If you’d prefer a simpler approach, you can roll your own data fetching “framework” by simply dispatching explicit actions when querying and receiving data from the network.

For example, lets say we’re building a blogging app. When querying for posts, we would dispatch POSTS_QUERY_INIT. The reducer could then update the state appropriately to indicate that the posts query is in progress.

postsQuery: {url: 'api.blog.com/posts',isPending: true,...}

In a thunk or saga, we would call fetch and when the promise resolves, we’d dispatch an action like POSTS_QUERY_SUCCESS or POSTS_QUERY_FAILURE. This would then update state appropriately to:

postsQuery: {url: 'api.blog.com/posts',isPending: true,data: [...],}

This example is far from thorough, but the idea is that by being explicit with Redux actions for each part of the request lifecycle, it becomes easy to debug any potential race condition or network error.

Handling other sources of non-determinism

In addition to network fetching, there are lots of other sources of non-determinism that can cause bugs. Luckily, we can use Redux for these as well to leave thorough logs in the event of a bug.

websockets

When listening on a websocket, we can dispatch an action whenever we receive data and reduce it into the store appropriately. For example:

myWebSocket.onmessage = function (event) {  store.dispatch({     type: 'BLOG_POST_UPDATE_RECEIVED',    payload: event,  } }

That way, when looking at the Redux logs for an error or user-reported issue, we can see all the data that was received over the websocket and, crucially, relate it in time to other redux actions and network requests.

Local Storage

Often, an app needs to read from local storage when it first starts up. To do this, you can use redux-storage a handy library that facilitates dumping state to local storage and reading/merging it back into state.

Whenever redux-storage loads or saves state from redux, it emits an action showing exactly the payload that will get reduced into the store.

Everything else…

The pattern of dispatching Redux actions for sources of non-determinism applies to most APIs like IndexedDB, or even functions like Date() and Math.random()- consider dispatching Redux actions with the result, so that you can easily debug these in the future.

React Router

Using react-router-redux lets you synchronize react-router state into Redux. Adding the integration is trivial, and doesn’t require any changes to how you use react-router. Once you get the library set up, you’ll see a new key in your Redux store called routing with information on the current router state.

In addition, react-router-redux dispatches actions like @@router/LOCATION_CHANGE when its state changes.

Also of note is that using react-router-redux lets you rewind router state when time-traveling in redux-devtools, since its state its state is derived from the state in Redux.

A note about local vs Redux state

I don’t want to get into the debate on local vs Redux state here, but production Redux logging does change the calculus of this decision in some cases. When deciding whether a given piece of state should be in Redux, ask yourself if seeing that state (and the actions that influenced it) could be helpful when debugging issues. If the answer is yes, consider putting that state in Redux so that it will be logged with crash reports and user issues.

Production Redux Logging

Logging Redux data in production helps you fix bugs and user-reported issues. Check out my previous blog post to learn more:

Redux Logging in Production_One of the greatest strengths of Redux is debuggability — by logging actions and state during an app’s execution…_blog.logrocket.com

TL;DR

Using libraries and patterns that put data through Redux helps build more debuggable applications by leaving a rich audit trail.

When architecting a new feature, ask yourself if it might be error-prone, and whether being able to view its state in the Redux logs would help solve a future bug.

It’s tough to keep up-to-date on front-end dev. Join our weekly mailing list to learn about new tools, libraries and best practices that will help you build better apps:

LogRocket is the JavaScript logging and replay tool that helps you fix bugs, faster. By capturing every log, network request and user session of your app, you can fix problems without back and forth.


Published by HackerNoon on 2017/06/28