React Native development tools - Part 2: Debugging tools
In order to follow this tutorial, you need to have a good grasp of the basic React and React Native concepts. Knowledge of Redux and Redux Saga will be helpful but not required. Those two libraries are used in the sample app that we’re going to debug.
In this tutorial, we will cover a couple of debugging tools which will help you uncover why your React Native app isn’t working as expected. Specifically, you’ll learn how to use the following tools:
- Reactotron
- React Native Debugger
This is the second of a three-part series on React Native development tools where we cover the following tools:
- Part 1: Linting tools
- Part 2: Debugging tools
- Part 3: Testing tools
Those two tools should cover most of the ground when debugging React Native apps. There are other tools which you can use, but we won’t cover them here today. We also won’t be covering tools for debugging the app performance, and the native side of things (for example: how to find the issue with a specific native module you’ve installed).
Prerequisites
In order to follow this tutorial, you need to have a good grasp of the basic React and React Native concepts. Knowledge of Redux and Redux Saga will be helpful but not required. Those two libraries are used in the sample app that we’re going to debug.
The debug app
To make the tutorial more hands-on, I’ve created an app which we will be debugging. By default, it’s not runnable. There are multiple issues that I intentionally placed in there so we can debug it.
Note that running the debug app isn’t required. If you only want to check out the tools and what they offer, feel free to skip this part.
Here’s what the app looks like once all the issues are fixed:
It’s a Pokedex app which shows a random Pokemon everytime you click on it. It uses Redux for state management, and Redux Saga to handle the side effects or potential failures that might be caused by the HTTP requests that it’s going to perform.
Here are the commands that you can use to run a copy of the debug app:
git clone https://github.com/anchetaWern/RNDebugging.git
cd RNDebugging
git checkout broken
react-native upgrade
react-native run-android # or react-native run-ios
In the above command, we’re switching to the broken
branch which contains the source code which has problems. The master
branch contains the working copy of the app.
Setting up Reactotron
The first tool that we’re going to setup is Reactotron, from the awesome guys at Infinite Red. Their tool allows developers to inspect React and React Native projects. Best of all, it’s available on all major platforms (Windows, Mac OS, Linux) in an easily installable file. Reactotron is created using Electron, that’s why it’s multi-platform.
I’m not going to go over Reactotron’s features and what makes it great (there’s the official website for that). So we’re going to go right into it instead.
At the time of writing this tutorial, the stable version is v1.15.0. You can try installing the pre-released versions from here but I can’t ensure that it will work flawlessly. So in this tutorial, we’re going to stick with the latest stable version instead.
The first step is to go here and download the zip file for your operating system. After that, extract it and run the executable file.
If you’re on Mac, I recommend using brew instead. I assume you already have brew on your system so I won’t go over that:
brew update && brew cask install reactotron
There’s also the CLI version which can be installed via npm. But in this tutorial, we’re going to stick with the desktop app instead.
More information on installing Reactotron can be found here.
Setting up React Native Debugger
React Native Debugger is like the built-in React Native Debugger, but on steroids. Every feature you wished were available on React Native’s built-in debugger is available on this tool, plus more.
Unlike Reactotron, React Native Debugger hasn’t reached version one yet (but it’s pretty stable) so you can download the latest release from the releases page. At the time of writing of this tutorial, it’s at version 0.7.18.
Just like Reactotron, it’s created using Electron so you can download the specific zip file for your operating system. After that, you can extract it and run the executable.
If you’re on Mac, you can install it with the following command:
brew update && brew cask install react-native-debugger
Adding the monitoring code
The tools you’ve just set up won’t work with the app automatically. So that we can hook up the tools to the app, we need to perform a few additional steps first.
The first step is to install the following packages in the project:
npm install --save-dev reactotron-react-native reactotron-redux reactotron-redux-saga
Those are the dev dependencies for Reactotron. While for React Native Debugger, we have the following:
npm install --save-dev redux-devtools-extension
Next, open the App.js
in the project directory and add the following right after the React
import:
import React, { Component } from "react";
/* previous imports here.. */
// add these:
import Reactotron from "reactotron-react-native";
import { reactotronRedux } from "reactotron-redux";
import sagaPlugin from "reactotron-redux-saga";
import { composeWithDevTools } from "redux-devtools-extension";
Reactotron.configure()
.useReactNative()
.use(reactotronRedux())
.use(sagaPlugin())
.connect();
const sagaMonitor = Reactotron.createSagaMonitor();
// update these:
const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); // add sagaMonitor as argument
const store = Reactotron.createStore(
reducer,
{},
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
Breaking down the code above, first, we include the Reactotron
module. This contains the methods for initializing the app so it can hook up with Reactotron:
import Reactotron from "reactotron-react-native";
Next, import reactotron-redux
. This is the Reactotron plugin which will allow us to monitor the state and the actions being dispatched:
import { reactotronRedux } from "reactotron-redux";
Another plugin we need to install is the reactotron-redux-saga
. This allows us to see the sagas and the effects that are triggered by the app:
import sagaPlugin from "reactotron-redux-saga";
For the React Native Debugger, we only need to import the module below. This is the redux-devtools version of Redux’s compose
method. This allows us to inject the method for monitoring the Redux’s global app state. That way, we can do time-travel debugging, inspect the state, and dispatch action from React Native Debugger:
import { composeWithDevTools } from "redux-devtools-extension";
Next, hook up the app to Reactotron:
Reactotron.configure()
.useReactNative() // set the environment
.use(reactotronRedux()) // use the redux plugin
.use(sagaPlugin()) // use the redux-saga plugin
.connect(); // connect to a running Reactotron instance
Next, set the sagaMonitor
as an argument to the sagaMiddleWare
. This allows us to monitor the sagas from the app:
const sagaMonitor = Reactotron.createSagaMonitor();
const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
Lastly, instead of using the createStore
method from redux
, we use the one from Reactotron
. Here, we’re also using the composeWithDevTools
method to inject the redux-devtools functions:
const store = Reactotron.createStore(
reducer,
{},
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
You can check this diff to make sure you’ve made the correct changes.
Debugging the app
Now we’re ready to start debugging the app. The first time you run the app, it will look like this:
As you’ve seen from the demo earlier, we expect a Pokeball image to be rendered on the screen. That way, we can click it and load new Pokemon. But in this case, there are none.
Throughout the tutorial, you might already have your ideas as to why something isn’t working. And you’re welcome to debug it without using any tools. But in this tutorial, we’re going to maximize the use of the debugging tools I introduced earlier to find out what the problem is.
The first problem is that the image isn’t being rendered. We can verify if this is really the case by using React Native Debugger. Launch it if it isn’t already. Then from your app, pull up the developer menu, and enable Remote JS Debugging. By default, this should pick up a running instance of the Chrome browser. But if you’ve launched React Native Debugger, it should pick that up instead.
If you have any running debugger instance on Chrome, close it first, disable remote JS debugging on your app, then enable it again.
Once the debugger picks up your app, it should look like this:
Everything above should look familiar because it has the same interface as the Chrome Developer Tools. Few tabs are missing, including the Elements, Timeline, Profiles, Security, and Audits. But there’s an additional UI such as the Redux devtools (upper left) and the component inspector (lower left).
What we’re interested in is the component inspector as this allows us to see which specific components are being rendered by the app. We can either sift through the render tree or search for a specific component:
As you can see from the demo above, we’ve searched for an Image
component but no results were returned. This verifies our assumption that the image is not really being rendered. From here, we can search for the component that we’re sure is rendering and then view the source:
The tool allows us to view the source file from which a specific component is being rendered. This is a very useful feature to have, especially if your project has a ton of files. You should avoid viewing the source for the RCTView
while debugging though, those are React specific views, and they won’t really help you debug your issue.
Upon inspecting the source code, we see that the image is depending on the fetching
prop to be true
before it renders. The fetching
prop is only set to true
when the app is currently fetching Pokemon data from Pokeapi. Removing that condition should fix the issue:
// src/components/PokemonLoader.js
{!pokemon && (
<Image
source={pokeball}
resizeMode={"contain"}
style={styles.pokeBall}
/>
)}
Once you’ve updated the file, the Pokeball should now show up on the screen:
But wait . . . it looks to be bigger than how it appeared on the very first demo earlier.
In cases where you want to quickly update the styles without updating the actual source code, you can edit the styles straight from the tool itself:
This will reflect the changes in the app as you make them. Note that this won’t automatically save to the source code so you still have to copy the updated styles afterward.
Now that the image is in its proper width and height. You can now click it to load a Pokemon…
But this time a new error pops up:
By the looks of it, it seems like the data that the Card
component is expecting isn’t there.
So our first move would be to check if the request is successfully being made. You can do that by right-clicking anywhere inside the left pane (where the redux-devtools and the component inspector is) and select Enable Network Inspect. By default, the network inspector is in the right pane (next to the Sources tab), but it won’t actually monitor any network request unless you tell it so.
As you can see from the screenshot above, the request to Pokeapi was successfully made. This means the problem is not in the network.
The next step is to verify if the data we’re expecting is actually being set in the state. We can check it from the Log monitor:
Looking at the above screenshot, it looks like the action is being invoked and the state is also updated. At this point, we already know that the cause of the error is that an incorrect data is set in the state. That’s why we got the undefined
error earlier.
At this point, you can dig through the code and find out what the issue is. But first, let’s see what sort of information Reactotron can offer us. Go ahead and launch Reactotron if you haven’t done so already.
By default, this is what Reactotron looks like. Most of the time, you’ll only stay on the Timeline tab. This will display the state changes, actions, sagas, and logs that you have triggered from the app. So you have to use the app so things will be logged in here:
Go ahead and reload the app, and then click on the Pokeball to initiate the request to the API. That will give you the following results:
From the screenshots above, you can see that Reactotron presents the error in a really nice way. Complete with line numbers where the exact error occurred. So now we have a much better idea of where the error occurred. This confirms our assumption from earlier that incorrect data is being set in the state. Now we know that the error occurred in the src/components/Card.js
file. This same error can be found on the error message displayed by the app. But I guess we can all agree that the one displayed by Reactotron is 50 times nicer and easier to read.
Upon further inspection of the things logged by Reactotron, we can see the actual contents of the API request (indicated by 0
and 1
in the out
object under CALL
). The app makes two HTTP requests to get all the details it needs: one for the general Pokemon data (name, types, sprite), and another for the description or flavor text.
Below that is PUT
. That displays the actual data that’s being sent to the reducer after the network request is made. Note that the value for out
is empty because the data isn’t actually being used in any of the components:
The above data is consistent with the one we found earlier in the React Native Debugger. But now we can clearly see that a pokemon
object is actually being sent to the reducer, along with the type
of action (API_CALL_SUCCESS
). That means the error must be in how the reducer makes the data available to the global app state.
If you look at the mapStateToProps
function in src/components/PokemonLoader.js
, you can see that it’s expecting pokemon
to be available in the state:
const mapStateToProps = state => {
return {
fetching: state.fetching,
pokemon: state.pokemon,
error: state.error
};
};
And if you open src/sagas/index.js
, you’ll see how the Pokemon data is being sent to the reducer:
function* workerSaga() {
try {
let pokemonID = getRandomInt(MAX_POKEMON);
const response = yield call(getPokemon, pokemonID);
const pokemonData = response[0].data;
const speciesData = response[1].data;
const englishText = speciesData.flavor_text_entries.find(item => {
return item.language.name == ENGLISH_LANGUAGE;
});
const pokemon = {
name: pokemonData.name,
image: pokemonData.sprites.front_default,
types: pokemonData.types,
description: englishText.flavor_text
};
yield put({ type: "API_CALL_SUCCESS", pokemon });
} catch (error) {
yield put({ type: "API_CALL_FAILURE", error });
}
}
From the code above, you can see that the worker saga is expecting the actual Pokemon data instead of one that’s wrapped around a pokemon
object.
This means that our assumption is correct. The problem is in the reducer. The one that’s actually making the data submitted from worker saga available to the global app state:
// src/redux/index.js
export function reducer(state = initialState, action) {
switch (action.type) {
case API_CALL_REQUEST:
return { ...state, fetching: true, error: null };
case API_CALL_SUCCESS:
return { ...state, fetching: false, pokemon: action };
case API_CALL_FAILURE:
return { ...state, fetching: false, pokemon: null, error: action.error };
default:
return state;
}
}
See the problem? The problem is that we’re not extracting pokemon
from the action
like so:
case API_CALL_SUCCESS:
return { ...state, fetching: false, pokemon: action.pokemon };
Remember that the action is being dispatched from the worker saga earlier. So action.type
is API_CALL_SUCCESS
. While action.pokemon
contains the Pokemon data:
// src/sagas/index.js
yield put({ type: "API_CALL_SUCCESS", pokemon });
It is then made available to the PokemonLoader
as props via mapStateToProps
:
// src/components/PokemonLoader.js
const mapStateToProps = state => {
return {
fetching: state.fetching,
pokemon: state.pokemon,
error: state.error
};
};
We then make use of it inside the component’s render
method:
// src/components/PokemonLoader.js
class PokemonLoader extends Component {
render() {
const { fetching, pokemon, requestPokemon, error } = this.props;
/* the rest of the existing code inside render method here... */
}
}
Once you’ve made the changes, the app should work as expected.
Conclusion
That’s it! In this tutorial, you’ve learned how to debug your React Native app using Reactotron and React Native Debugger. Specifically, you’ve learned how to inspect components (and their data), monitor network requests, state, actions, and sagas.
There are a lot more features that we haven’t taken a look at. Things like time-travel debugging, dispatching of actions, breakpoints, logging custom messages, and many more. I’ll be leaving those for you to explore.
Stay tuned for part three where you’ll learn about testing tools.
8 November 2018
by Wern Ancheta