Skip to content

Testable state management using React Native with MobX

This article will go through how to manage the state of your React Native application and test it. We will be using the MobX state management library, Jest testing framework and React Native Testing Library.

State management is the core part of your React Native application and can very quickly become unmanageable if not done properly. When starting a new project, you should always consider different options and find out what is best for your needs.

For simple applications with only a couple of global variables, such as theme or language selection, you could go with Context API. For more complex needs, with multiple global variables combined with networking and persistence, you could choose Redux. But today, we will be using MobX.

MobX

MobX is a state management library that can be used with different JavaScript frameworks, but is mostly used with React. It does not bind you to a specific architecture and allows you to manage your application state outside of any UI framework.

“Anything that can be derived from the application state, should be. Automatically.”

The philosophy behind MobX, as they put it, is to be straightforward, have effortless optimal rendering and not bind the user to any specific architecture.

Straightforward

Writing code should be as minimalistic as possible. No boilerplates should be needed. Because when the project is kept simple, it’s less likely to run into technical debt over time. The reactivity system of MobX will automatically detect all changes made by the user and propagate them where needed. No special tools are needed.

Effortless optimal rendering

No need to manually optimize your components to prevent unnecessary rendering. MobX tracks all the changes and uses of your application data in runtime, building a dependency tree that captures all relations between state and output. This guarantees that rendering only happens when actually needed.

Freedom of architecture

Letting you manage the application state outside of any UI frameworks. As this makes your code easily portable and decoupled, it will be especially useful when we start writing tests for our application.

Jest

Jest is a JavaScript testing framework. Jest requires a minimum amount of configuration and provides a comprehensive toolkit that can be used without significant modifications to existing code. It is well documented, actively maintained and constantly improved. The default React Native template is shipped with a preset of Jest tailored to this specific environment.

React Native Testing Library

React Native Testing Library is an open-source library created to help write maintainable tests for React Native applications. React Native Testing Library is designed to be used with Jest but can be used with other testing solutions as well.

Example

Let’s create a simple Todo example.

Create a new project:
npx react-native init MobXExample –template react-native-template-typescript

Install MobX:
yarn add mobx mobx-react-lite

Then let’s think about what we need to build a simple MVP Todo app and divide the user interface implementation into different components. We want to be able to add todos. We want to see all the todos. We want to see if a todo is completed or not. We want to know how many todos we have. We want to know how many todos we have completed.

Header
    Title component
        Count component with total and undone count
Todo list
    List
    List item
        Tick component to display done or not done
Footer
    Input
    Button

I’m not going to copy-paste all the code here, but you can find the whole source code here. I strongly suggest that you check it out before moving forward. It will make this example easier to follow and give you a better understanding of what is going on here.

Next, we need to plan how to store the todos and what type of data we want to store. So we need a list that contains our todos. And we need to be able to differentiate the todos from each other. Then we need to be able to know what each todo is all about, and we want to know if it has been completed or not. So here is an example of what a todo store could look like.

export class Todo {
 id: string = uuid();
 title: string;
 completed: boolean = false;
 
 constructor(title: string) {
   this.title = title;
 }
 
 complete() {
   this.completed = true;
 }
}
 
export class TodoStore {
 todos: Todo[] = [];
 
 get totalTodos() {
   return this.todos.length;
 }
 
 get completedTodosCount() {
   return this.todos.filter(todo => todo.completed === true).length;
 }
 
 addTodo(title: string) {
   this.todos.push(new Todo(title));
 }
}
 
export const todoStore = new TodoStore();

Looks simple, doesn’t it? And you might be thinking: “what about MobX?” Good question, as it’s not there yet. So let’s add the MobX to make our app react to changes automatically and display the changes.

We need to make a “store observable” for MobX to be able to track all changes made to the data. This could be implemented by using the makeObservable function and annotations (observable, computed, action) from the MobX module. There is a makeAutoObservable function that can do everything automatically but, for the sake of learning, we will do it manually.

Last, but not least, we need to make sure our components know when to render again. That will happen by wrapping our components in an observer high order component (HoC) from the mobx-react-lite package. A high order component is a React technique for reusing component logic. Simplified HoC is a function that takes a component as a parameter and returns a new component.

Observable

The observable attribute defines a field that stores the state for MobX to track. Any change in the field marked as observable will tell MobX to start doing its magic.

Action

All methods that modify the state, and no others, should be marked as actions. This will guarantee that intermediate or incomplete values are not visible to the rest of the application. It will also make it easier to identify where the state updates happen.

Computed

Mark your getters as computed. They are used for deriving information from observables. Output is cached automatically and changed only if the underlying observables change.

Reactions

There are also reactions (autorun, reaction and when) in MobX, but we are not utilising them in our example. Reactions are used to automatically run side effects when the state changes in some relevant manner.

Observer

Wrapping your component into the observer means making sure that it will render again when there are changes to relevant data. You don’t need to call anything manually or figure out how to subscribe to specific pieces of information. Observer HoC will do all that for you.

Let’s consider our example store. In TodoStore, the value that the user can change is todos, so that should be annotated as observable. Then we have two getters: total number of todos and number of completed todos.

These two getters should be annotated as computed, so that MobX knows to recompute them if there are changes to the observable todos. And then we have a method to add a todo. Because the method changes the state, it should be marked as an action.

The same logic should be applied to Todo. Users can change the completed state, so annotate it as observable. And the ‘complete’ method, which we are using to change the completed value, should be annotated as an action because it modifies the state. That’s it. You have now implemented MobX in your store, and this is how it should look like:

export class Todo {
 ...
 constructor(title: string) {
   this.title = title;
   makeObservable(this, {
     completed: observable,
     complete: action,
   });
 }
 ...
}
 
export class TodoStore {
 ...
 constructor() {
   makeObservable(this, {
     todos: observable,
     totalTodos: computed,
     completedTodosCount: computed,
     addTodo: action,
     fetchTodos: action,
   });
 }
 ...
}
 
export const todoStore = new TodoStore();

Next, we need to find all the components that are using data from TodoStore. All those components need to be wrapped within the observer higher order component (HoC). So we have Header and TodoList observing TodoStore, and TodoListItem observing a specific Todo. All three of these need to be wrapped within observers.

Here is the TodoList component, for example:

export const TodoList: FC<ITodoList> = observer(({todos}) => {...});

We now have a simple MVP version of a todo application. Moving forward, it would be cool to add some more features and functionalities. However, we also need to make sure that future development will not introduce bugs to the existing code. How can we achieve that? By writing some tests.

Testing

Anyone can make mistakes and introduce bugs. Testing helps us to catch our bugs and mistakes before they are distributed to the end user. Writing a test requires the code to be testable.

This means thinking about testing when creating your components. Instead of making one big component and file with everything in it, you should design smaller pieces with multiple different components. That makes it easier to test individual components more thoroughly and locate bugs.

Also try to move your app logic, state and data fetching out of components as much as possible. This way, you can keep business logic testing independent from the components.

Setup

Jest should already be installed in your project because React Native comes with a Jest preset tailored to this specific environment. If it’s not or you have started your project in some other way, see the Jest installation guide.

Next, we want to install the React Native Testing Library and additional matchers specific to React Native.
yarn add –dev @testing-library/react-native @testing-library/jest-native

To use the additional matchers, you need to do a little setup first. So let’s create a Jest folder in our project root and create a setup file in that folder. We will store all our Jest setup files in the Jest folder. Then you need to tell Jest to use them, so add the folder to the Jest config in the package.json file.

You can now test whether your setup worked by running the command yarn run test. You might encounter the following error: cannot find module ‘react-dom’. You can fix it by adding moduleNameMapper to your Jest config. We will not go into this in any more detail, because that’s not the point of this article, but you can find additional information here and here.

Snapshot

Snapshot testing is a powerful way to prevent unwanted UI changes from getting into the released application. A snapshot test creates a readable, textual representation of your component and then compares it to the previous snapshot from an earlier test run. If the previous and current snapshots do not match, the test will fail.

A failed test means that the UI has changed and the developer needs to validate the changes. If the changes are intended, the snapshot can be updated, and if they are an unintended bug, the failing code needs to be fixed.

Code structure and quality play a crucial role in writing snapshot tests for your components. If the component is very big, so is the snapshot, and reading it will be next to impossible. You also need to pay extra attention when creating the first snapshot of your component. Even if the initial component output is incorrect, the snapshot will still register as correct.

Unit test

Unit tests are meant to test the smallest parts of the code like individual functions, classes or single components. Unit tests are written to ensure that all non-UI-dependent functionalities in each of the components are working as expected.

If one unit depends on another, you will most likely end up mocking the other one. Put simply, mocking means that you are isolating the test subject from the others by creating a simulated replacement for the units not in testing. Or you can use spying. Spying differs from mocking in that it only replaces parts of the unit, while mocking replaces the whole unit.

Integration test

Integration tests test multiple units at the same time to make sure that they are working together as intended. That is why you should avoid mocking when writing integration tests. You can still use mocking for the initial data or when code being tested relies on network responses, for example.

When relying on network responses, it would actually be a good idea to mock API calls and responses in the tests so that your test code is not dependent on an active network connection.

Creating an example test

We will be writing a couple of unit and integration tests, but not for every single case in this example. However, this article aims to cover everything you need to do so. You can find more tests for each component in the source code.

Writing a snapshot test for a component is very simple. Here is an example for MainScreen. You can do this to all of the other components too if you want.

describe('MainScreen testing', () => {
 it('renders correctly', () => {
   const tree = renderer.create(<MainScreen />).toJSON();
   expect(tree).toMatchSnapshot();
 });
}

For testing to be possible, you need to be able to find certain elements in the view hierarchy. RNTL provides us with a render function that returns multiple different helpers to query the output components’ structure. In the above test, for example, we are using the getByText query to find the specific element and then expect it to be correct.

For testing to be meaningful, we need to validate the result. Jest provides us with an expect function for that purpose. The expect function gives you access to a lot of different matchers to validate almost anything you need. For even more extensive matcher needs, there is a community-driven library called jest-extended. We will be using at least one matcher from the jest-extended library, but we will get back to that later.

Let’s continue writing some other tests than snapshots. The Title and TodoCount components are practically the same and operate according to the same logic: taking a prop and displaying it in a Text component. We expect the components to display the correct text according to the prop we passed to them. Here is an example test for Title:

describe('Title testing', () => {
 it('Title text to be correct', () => {
   const {getByText} = render(<Title title={'Test title'} />);
   const text = getByText('Test title');
 
   expect(text).toHaveTextContent('Test title');
 });
});

The same testing logic can be applied to the TodoList component. You pass a list of todos as a prop and expect them to be displayed. Remember, the TodoList also needs to be able to work when empty. Try to write the discussed test cases. If you are having trouble, check the source code for help. I’ll give you a tip: queryAllBy* and toHaveLength.

We will do one test for TodoListItem to verify that the complete function is called when the user presses the list item. Because we don’t want our TodoListItem component test to be dependent on anything but itself, we need to do a mock implementation of the Todo class. Below is an example of how to do it. If you like, you can also read more about mocking non default class exports.

const mockComplete = jest.fn();
jest.mock('../src/store/TodoStore', () => {
 return {
   Todo: jest.fn().mockImplementation(() => {
     return {
       id: '',
       title: 'Test Todo',
       completed: false,
       complete: mockComplete,
     };
   }),
 };
});
 
beforeEach(() => {
 Todo.mockClear();
});

Also be mindful of how you name your mock function. Try to always name your mock functions beginning with mock*, for example ‘mockSomething’. In some cases, Jest will throw an out-of-scope error.

You might have noticed that we are using something called beforeEach and mockClear. With beforeEach, we can run a function before every test. In this example, we are using it to clear the Todo mock to make sure there is nothing left from the first test before moving to the next.

Now that we have mocked Todo, we can write the test for TodoListItem. Import the Todo into your test file just as anywhere else. Add the mock from above. When you use the import in your tests, Jest will now recognize the mock and use that instead of the real class.

import {Todo} from '../src/store/TodoStore';
 
const mockComplete = jest.fn();
jest.mock('../src/store/TodoStore', () => {...};
beforeEach(() => {...});
 
describe('TodoListItem testing', () => {
 it('Complete todo tick to be green', () => {
   const {getByTestId} = render(<TodoListItem todo={new Todo('Test Todo')} />);
   fireEvent(getByTestId('CompleteTodoButton'), 'press');
   expect(mockComplete).toHaveBeenCalled();
 });
});

We are using the getByTestId query in the TodoListItem test to make sure that we will find the pressable. This means that you need to add a testID prop to the pressable in the TodoListItem component. After we have found the pressable, we need to simulate the user pressing it. RNTL provides a really helpful tool for this, called fireEvent, which allows us to simulate the onPress event.

We then have the TodoStore. As this is the very core of our application, you should pay special attention to testing it. Test everything there is to test. Also, testing the store is much simpler than testing components.

A couple of examples are provided below, and you can find more in the source code. First, we want to test the store initializations. Because we have no persistence, the store should be empty of data. We can then test if adding a todo is having the intended effect. Adding a todo should increase the length of the todos array and the value of total todos.

describe('TodoStore testing', () => {
 it('constructs correctly with empty todoslist', async () => {
   const store = new TodoStore();
 
   expect(store.todos).toHaveLength(0);
   expect(store.totalTodos).toBe(0);
   expect(store.completedTodosCount).toBe(0);
 });
 
 it('add todo works', () => {
   const store = new TodoStore();
 
   store.addTodo('Test todo');
 
   expect(store.todos).toHaveLength(1);
   expect(store.totalTodos).toBe(1);
   expect(store.completedTodosCount).toBe(0);
 });
});

Bonus

Because most apps will use some API calls, I will show you how to mock and test them. I’m not going to explain everything, but I will provide all the necessary links to find all the information you need. You can find the full example in the fetch-mock branch.

Let’s install and set up a jest-fetch-mock library. That will make testing API calls really easy. First install the library yarn add -D jest-fetch-mock. After installation is complete, you need to set it up like we did before, with the additional matchers.

Now comes the coding part. We will add one example fetch, simulating a situation in which the user can retrieve saved todos from a server. You can find the code in the TodoStore. Next, we will write one test to make sure that it works in all circumstances.

Pay attention to runInAction in the TodoStore and make sure to check the resetMocks, mockResponseOnce and mockReject in the test.

And that’s it. I hope you liked the article and found it informative. Maybe it even helped you a bit on your way.

Search