Published on
Time to read
9 min read

All you need is mobx-react-lite

Night view of a big city
Photo by Devin Avery on Unsplash

The popularity of MobX is getting higher and higher. So does the need to decrease the size of the bundle. And not only that, but we also seek to write cleaner and maintainable code in our projects. So how can we achieve this?

Since the React team released hooks from v16.8.0 — we were introduced to a new nice way to write the components in a functional way. In my personal opinion — the use of the hooks over the HOCs looks way cleaner. Accessing the data from the store with hooks also seems cleaner. We tend to write less code with functional components compared to big chunks of code that we used to write sometimes with Class-like components. That is one of the ways to increase the maintainability of the code.

What about bundle size?

Using lightweight package alternatives is a great way to reduce the final bundle size. And we can do that with many packages. Remember using Moment.js? I don’t recall using it for a while because it has a way bigger bundle size than the alternative date-fns package. Even tho the Moment still has twice more downloads and stars.

Comparison of Moment.js and Date-fns packages
https://www.npmtrends.com/moment-vs-date-fns

Mobx-react-lite is a lightweight binding to glue Mobx stores and functional React components. And the size of it is smaller, but maybe not as much as you would expect. Nevertheless — this is all we need.

Comparison of mobx-react and mobx-react-lite packages
https://www.npmtrends.com/mobx-react-vs-mobx-react-lite

Let’s get started

We will need a React project setup and two dependencies to get started:

yarn add mobx mobx-react-lite
npm install mobx mobx-react-lite

For a simple example, we can create a Counter Store with some actions and values. I’m using Typescript with the feature enabled to support the decorator's syntax, but it does not matter if you are using decorators or functions for our case.

CounterStore.ts
import { action, observable } from "mobx";

export class CounterStore {
  @observable
  value: number = 0;

  @action
  increment() {
    this.value += 1;
  }

  @action
  decrement() {
    this.value -= 1;
  }
}

In order to access the store from the component we will need to create the instance of the store and, of course, share it in some way with components. React Context is a quite good fit for this task and we can leverage it for our needs. Let’s create a stores.ts file with the store instances and Context wrapper.

stores.ts
import React from "react";
import { CounterStore } from "./CounterStore";

export const stores = Object.freeze({
  counterStore: new CounterStore()
});

export const storesContext = React.createContext(stores);
export const StoresProvider = storesContext.Provider;

Good. Now we have a stores variable to which we can save instances of the Mobx stores. We freeze the object in order to avoid any unexpected changes in it. But, of course, this is an optional step.

We created a React Context based on the store’s variables and also created a Stores Provider component, which we will use shortly.

Now, let’s update the root index.tsx file with a Context Provider wrapper. Follow this code snippet.

index.tsx
import * as React from "react";
import { render } from "react-dom";

import App from "./App";
import { StoresProvider, stores } from "./stores";

const rootElement = document.getElementById("root");
render(
  <StoresProvider value={stores}>
    <App />
  </StoresProvider>,
  rootElement
);

Very well. At this point, we have a place, where we keep our stores. We also have a way to share the stores with the components, but not completely. To access that context we have to create two custom and quite handy hooks. One will return all stores, and another — a specific store of preference.

hooks.ts
import React from "react";
import { stores, storesContext } from "./stores";

export const useStores = () => React.useContext(storesContext);

export const useStore = <T extends keyof typeof stores>(
  store: T
): typeof stores[T] => React.useContext(storesContext)[store];

Alright, now we have it. See that weird type definition for the useStore hook? It would give us the proper types for the passed store key.

signature.ts
<T extends keyof typeof stores>(store: T): typeof stores[T]

We will only accept store variable type if it’s one of the keys of the stores object. So, in our case, it will only accept a counterStore string and will return the corresponding type of the given store. Nice, isn’t it?

Ok, let’s finally access the store, its methods, and properties from the actual component. To do that we have to modify App.tsx file with the custom hooks and Mobx Observer wrapper.

App.tsx
import * as React from "react";
import { useStore } from "./hooks";
import { observer } from "mobx-react-lite";
import "./styles.css";

const App = observer(() => {
  const counterStore = useStore("counterStore");

  return (
    <div className="App">
      <h1>Counter app</h1>
      <button onClick={() => counterStore.increment()}>Increment</button>
      <button onClick={() => counterStore.decrement()}>Decrement</button>
      <h2>Value {counterStore.value}</h2>
    </div>
  );
});

export default App;

We access the counter store with its key and get the store in return. Also, we have to wrap the component into the observer function to allow the component to listen for the store changes. But still, we do not destruct stores.

App.tsx
    const counterStore = useStore("counterStore");
Counter application demo

The app is working as expected, cool! Let’s see the TypeScript hints that we got from this setup and the ways it will cover our backs from doing wrong things.

The first thing to mention is the code completion feature. When we will add the useStore hook and start passing a string there as an argument — a key hint will be shown. We also will not be able to pass non-existing store keys. Nice!

TypeScript hint for the useStore hook
TypeScript hint
TypeScript warning on wring store name passed to the hook
TypeScript warning

This also works for the case when we have multiple stores as well. We can access them with a separate useStore hook or with a useStores hook as well. Check this out.

TypeScript autocomplete for the possible store keys
TypeScript hint for multiple options

Testing

Sure thing we need to test this. In this article, we won’t make tests for the store itself, since this is a topic of a different nature.

I find it easy enough to use Jest alongside React Testing Library. So, let’s add the necessary dependencies.

yarn add @testing-library/react-hooks react-test-renderer -D
npm install @testing-library/react-hooks react-test-renderer --save-dev

Cool! Now we can start testing our hooks file. Nothing too fancy here, but still necessary.

hooks.test.ts
import React from "react";
import { renderHook } from "@testing-library/react-hooks";
import { stores, StoresProvider } from "./stores";

import { useStore, useStores } from "./hooks";

describe("useStores", () => {
  it("it return complete stores map", () => {
    const wrapper: React.FC = ({ children }) => {
      return <StoresProvider value={stores}> {children} </StoresProvider>;
    };

    const { result } = renderHook(() => useStores(), {
      wrapper
    });

    expect(result.current).toStrictEqual(stores);
  });
});

describe("useStore", () => {
  it("it return a store by key", () => {
    const wrapper: React.FC = ({ children }) => {
      return <StoresProvider value={stores}> {children} </StoresProvider>;
    };

    const { result } = renderHook(() => useStore("counterStore"), {
      wrapper
    });

    expect(result.current).toStrictEqual(stores.counterStore);
  });
});
Hooks file tests results where all tests are green
Hooks file tests results

Component tests

To do that we will need a few more dependencies. To test the components and to perform user events.

yarn add @testing-library/react @testing-library/user-event -D
npm install @testing-library/react @testing-library/user-event --save-dev

Good! Let’s test the components now. We can mock the useStore hook to always return a specific store using Jest. And, we can be type-safe with a few extra lines of code. For every test we want the hook to return a new store. But, for one of the tests, we will change that slightly and replace the initial value of the counter. Check this code snippet.

App.test.tsx
 import React from "react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import { useStore as storeHook } from "./hooks";
import App from "./App";
import { CounterStore } from "./CounterStore";

const useStore = storeHook as ReturnType<typeof jest["fn"]>;
jest.mock("./hooks");

const stubCounterStore = () => {
  const store = new CounterStore();

  return store;
};

describe("FilterComponent", () => {
  beforeEach(() => {
    useStore.mockReturnValue(stubCounterStore());
  });

  it("it renders without error", () => {
    expect(render(<App />)).toBeTruthy();
  });

  it("it renders initial counter value different from 0", () => {
    const store = stubCounterStore();
    store.value = 10;
    useStore.mockReturnValue(store);

    const { queryByText } = render(<App />);

    expect(queryByText("Value 10")).toBeTruthy();
  });

  it("it call the increment store action in the increment button click", () => {
    const { getByText, queryByText } = render(<App />);
    const incrementButton = getByText("Increment");

    expect(queryByText("Value 0")).toBeTruthy();
    expect(queryByText("Value 1")).toBeFalsy();

    userEvent.click(incrementButton);

    expect(queryByText("Value 0")).toBeFalsy();
    expect(queryByText("Value 1")).toBeTruthy();
  });

  it("it call the decrement store action in the decrement button click", () => {
    const { getByText, queryByText } = render(<App />);
    const decrementButton = getByText("Decrement");

    expect(queryByText("Value 0")).toBeTruthy();
    expect(queryByText("Value -1")).toBeFalsy();

    userEvent.click(decrementButton);

    expect(queryByText("Value 0")).toBeFalsy();
    expect(queryByText("Value -1")).toBeTruthy();
  });
});
App file tests results where all tests are green
App file tests results

Quite simple, isn’t it? Yep! And we got a green light as well!

Summary

Mobx-react-lite is all I need in my projects. Would this be your choice of favor as well? Let me know in the comments below 😉.

The entire project is available on the GitHub link.