Reactive State 1 - An Alternative Way To Manage State in React Apps (ft. mobx)

Reactive State 1 - An Alternative Way To Manage State in React Apps (ft. mobx)

Reactive State - 1(ft. mobx) (2) (main).jpg

When building a React application, one of the challenges you will need to deal with, is state management. While managing state within a component can be easily done, having to manage state that will be shared across components can become a bit challenging. While using Redux which follows the flux pattern, became a very popular solution to this problem, there are other solutions out there that try to address the same problem using a different approach/model, one of which is reactive and atomic model. Rather than having one global giant store with a clear and predictable flow on how the store should be updated (although good, it introduces a bit more verboseness and complexity), your state can be considered more like in smaller stores/atoms and can be updated automatically and atomically while being observed by components that need the updated state. some of the libraries that rely on this model include, mobx, mobx-state-tree (built on mobx) and Recoil. This article attempts to explain how you can manage state shared across your React components using mobx and when you may possibly want to. You can check out just the TLDR section if you wish to see just a summary or overview. There is also a demo section that goes further to show you how to.

NB: This article does not go as low-level, as to explain mobx-internal but basically how it integrates and can be used optimally with React.

TLDR

  • setting up state with mobx is as simple as defining your state value as an observable and listening for changes to it, in the components where its being used.

  • mobx-react provides us with an observable HOC that we can use to wrap every component that reads a value from our observable store. This will enable the component to listen for change and be updated every time the value from the store being read inside the component, changes.

  • To make values observable, mobx-react leaves us with several options as to how we can make our value/data observable. It includes using useState with observable class or useState with local observable object or useLocalObservable hook. In the demo section we see how to create an observable store using the useLocalObservable hook and providing through React.Context.

  • Components wrapped with observer only subscribe to observables used during their own rendering of the component. So if observable objects / arrays / maps are passed to child components, those have to be wrapped with observer as well.

  • To pass observables to a component that isn't an observer, either because it is a third-party component, or because you want to keep that component MobX agnostic, you will have to convert the observables to plain JavaScript values or structures before passing them on, this can be done using a toJS utility method from mobx. (we will see this in the demo section).

  • mobx provides us with flow that allows us handle asynchronous requests using generator functions. (see more details in the demo section).

  • mobx also allows us compute values i.e derive information from our observables based on it current state. They evaluate lazily, caching their output and only recomputing if one of the underlying observables has changed.

  • You can have multiple Observable value stores in your application.

  • mobx is designed optimally and only re-renders when a value being read in a component changes, this means that, observables that are accessible by the component, but not actually read, won't ever cause a re-render. In practice this makes MobX applications very well optimized out of the box and they typically don't need any additional code to prevent excessive rendering.

NB: When using mobx-react, it is recommended to dereference values (read values) as late as possible, because MobX will re-render components that dereference observable values automatically. If this happens deeper in your component tree, less components have to re-render. e.g <CharacterName={character.name} /> will be slower than <CharacterName character={character} /> (recommended).

DEMO

we will setup a marvel heroes demo app and we will be using mobx to manage app-level state. You can find the complete code for this demo, here(on the branch named mobx). Also to run this project you will need to get a private and public key from marvel and store it in src/env.local as REACT_APP_PRIVATE_KEY and REACT_APP_PUBLIC_KEY. Remember to also install the dependencies in package.json file.

marvelDemo.png

current project structure Here is the current project structure on the master branch

Screenshot (3).png

we already have our components and routes set up. We will then setup our app-level state using mobx and mobx-react-lite (binding library to use mobx with React).

we will setup our mobx state in the path src/mobx/marvelState

NB: mobx-react-lite was used for this instead of mobx-react, this is because mobx-react-lite only supports usage with functional components which is sufficient for our use case and is lighter. mobx-react can be used in all cases though as it supports both class and functional components.

Installation and setup:

To install mobx and mobx-react-lite, run:

npm install mobx mobx-react-lite

setting up a store/observable

To setup our observable store, we use the useLocalObservable hook from mobx-react-lite to create our observable state values and use React context to provide the value across our app components.

import React, { createContext } from "react";
import { useLocalObservable } from "mobx-react-lite";

//creating our store context
export const MarvelContext = createContext();

export const MarvelProvider = ({ children }) => {
  const marvelStore = useLocalObservable(() => ({
    //state values
  }));
  return (
    <MarvelContext.Provider value={marvelStore}>
      {children}
    </MarvelContext.Provider>
  );
};

our state values will include a characters array, a loading state, an error state, a searchCharacters function for fetching the marvel heroes characters, and a numOfCharacters computed value.

Screenshot (8).png

Wrapping your React component tree with the provider component holding your mobx state

since we are using React Context, we use the provider component that holds our mobx state to wrap our react component tree, note that these must not be at the root of your component three, like in redux, just make sure it is placed at a position in your component tree, where all the components that will read values from it, are its children. In our case here, we will just put it at the root, in App.js

import { MarvelProvider } from './mobx/marvelState';

const App = () => {
    return (
        <MarvelProvider>
            //children components that will read the state values
        </MarvelProvider>
    )
}

export default App;

Providing your components with the state values from Context

Finally to provide our components with our state values, we do this through react context

const { characters, loading, numOfCharacters } = useContext(MarvelContext);

and also wrap the component where you read values from the mobx store with the observer function from mobx-react-lite. In src/components/Characters/Characters.js

Screenshot (10).png

similarly in our src/components/Characters/Search.js file, we bring in the searchCharacters function from our mobx state, which we will call to trigger our search. We don't need to wrap the Search component with observer from mobx, because we are not rendering any value from mobx state in our search component.

conclusion: We've seen how mobx can be used to manage app-level state in a react application. Without much boiler plate we were able to set up app-level state that comes with built in optimizations for our re-renders. However, you would notice that mobx allows for mutuable state update, although you can still decide to do that immutably, cause mobx is unopinionated and leaves you with lots of options. For situations where you will need to enforce Immuatbility (maybe its a large team and state update needs to be more predictable and traceable) without lossing the reactiveness and performance of observable data, mobx-state-tree comes in very handy. Will demo how to use mobx-state-tree in my next blog post.