Should You Use a State Library in JavaScript UI Apps

architecture react state Mar 16, 2021

In the world of JavaScript, there are a lot of useful state management solutions like MobX or Redux. But recently, developers are tempted to use simple solutions built into their UI libraries — like React context API. Is it any better? Or, is it worth using state libraries which could provide simpler solutions?

What is React Context

Let’s discuss React context API first. React context is a way to share properties between components in a React tree. In general, it sounds perfect — you are creating a context provider component that can store some state, and under it, no matter how deep, you can access that value. It also provides the possibility of rerendering components whenever the state in context changes.

Sounds nice? Yes, it is because it can clean up props provided to the component. But is it the right solution for state management? Let’s look at the alternative…

State management libraries

The other solution is to use state management libraries. These are battle-ready solutions that make it easy to keep state, update it and inform a set of subscribers about updates. You can find well-known state libraries such as MobX or Redux. Some also consider RxJS a state management library (it’s not), but it can be a base of a nicely built state solution. In my opinion, when thinking about large-scale frontend solutions, it’s better to use them instead of simple built-in framework approaches like context. Let me explain why.

Framework Agnostic

When you use MobX or Redux, you are not limited to one frontend framework. You can use whatever you want — React, Vue, Angular, jQuery, Polymer, Svelte, Mootools, YUI, the list can go on and on… It will even work with vanilla JS and NodeJS backends. It means that you can change your UI framework when needed without rewriting all state-related logic. In the JavaScript world, where we get new frameworks nearly every day, it’s worth securing yourself to have less code to change in the future if needed!

I believe that code tells more than a thousand words, so let’s see on the example how we can use the same state in two completely different ways — in React and with vanilla JS:


// React
export const StateDisplay = observer(({ store }) => {
  return <button onClick={store.increment}>{store.value}</button>;
});

// Vanilla JS
const button = document.createElement("button");
document.getElementById("writeHere").appendChild(button);
button.addEventListener("click", () => store.increment());
autorun(() => (button.innerText = store.value));

State Externalisation

If the state management library is framework agnostic, it means you can use it outside of the UI. I’m not neccessarily talking here about the backend. For the sake of having clean code and neat, testable architecture, it’s worth keeping your logic outside of components. If you still remember the MVC model, which was quite popular among JS developers with the rise of AngularJS, we always separated state (the M — model), logic (the C — controller), and a view (the V - view). How does this relate to a framework like React? Due to using React context, developers keep logic inside UI components, therefore merging V and C into one file. What’s more, context alone is also a component, so you end up having M in V too.

Sounds confusing? Yes, it is. Fortunately, state management libraries don’t limit us to use them only in components. We can use them wherever we want in our code. Is it a component? Is it some service? It doesn’t matter.

Easily Testable

If we use state management libraries outside of UI components it gives us better testability, we can test any logic related to them without any need to render or mock UI. For example testing reducers and actions in Redux is very simple, because it’s a plain JS. The same goes for MobX actions and computeds (Mobx’s mechanism for producing dynamic state trees).

So, what is the problem when one wants to test the state saved in something like React context? First of all, we need to render the whole UI or at least a component that keeps logic in order to test it. We can do this either by using tools like Cypress that run a real browser or with mocking tools like Testing Library. But when this happens, it’s not a unit test anymore. Because by unit testing, we think of writing tests for the smallest possible part of an app, like single state action or selector. When we need to render the whole app or many components to perform some action, can we still call it a unit test? I don’t think so in my opinion, we are entering the domain of integration or end-to-end tests.

Once again, take a look into the code. Let’s compare the test, where we are testing state logic directly (as we can with MobX/Redux) and via component (as with React Context).


test("should increment a value (MobX)", () => {
  const store = new Store(0);
  expect(store.value).toBe(0);
  store.increment();
  expect(store.value).toBe(1);
});

test("should increment a value (context)", () => {
  render(
    <StoreContextProvider>
      <Component />
    </StoreContextProvider>
  );
  const displayElement = screen.getByTestId("display");
  const button = screen.getByRole("button");
  expect(displayElement).toHaveTextContent("0");
  userEvent.click(button);
  expect(displayElement).toHaveTextContent("1");
});

You have to admit, that we are comparing a simple, plain unit test (arrange store, act by doing action, assert the result) to a more complex test, doing unnecessary things like rendering and DOM traversal. In this case it may still look easy, but when application grows and event handlers get more complicated, testing component may become painful to write (and surely won’t be UNIT testing!)

Enforcing Good Architecture

We can automatically get better architecture and testability by using state libraries that force developers to use better architecture. Let’s talk here about how it’s done in the two most popular options — MobX and Redux.

MobX itself is not very strict in architectural terms, giving us some freedom, but we end up with a nice store containing state. The most popular and recommended way to use MobX is an object-oriented one. In this approach, we have a class containing a state, called store, with the definition of actions and computed values. It’s a very clean approach, where we have everything inside one class — state contents, actions, flows, computeds. That last one is worth mentioning, because it provides us the way to compute values based on the current state.

On the other hand, Redux is based on Facebook’s Flux architecture but is not its direct implementation. Here we are ending with functional approach to the state. Apps written with it have a very clean division into actions, reducers, state, selectors.

So, why is React context not as good as these simpler state libraries? Well, it’s important, they are not designed to be state management solutions. They can be used in this manner, but in general, it’s not what they were designed for. That means you have to find your way to store the state, update it, and divide data in it. It too often ends up with one big context for everything and with too much logic inside components. It doesn’t help us create a ‘clean’ architecture.

Of course, you can argue that it’s not the fault of Context but developers who use it, I can partially agree. Even if we do the best practices with context, we are still having everything coupled inside UI framework. On the other hand, it’s harder to have a malformed MobX/Redux solution than malformed Context solution. So, if something can help us keep good architecture — use it.

Summary

While it’s very tempting to use built-in solutions like React Context API, you may want to think about whether it will be the right choice (because these will create coupling in your view layer which makes your testing hard and your architecture very opinionated). When your app is going to be large, it’s worth thinking about a good architecture from the start, and state management is an integral part of it. Most of the apps are data-centric, therefore it’s important to properly keep the state. We also need to keep in mind, that most critical parts of the app are often those operating on the state, so we should be able to test it without unnecessary problems.

Suppose you want to learn how to do a scalable and testable JavaScript UI app. In that case, you may want to check our 12-Week UI Architecture Academy to master your frontend skills and learn the framework-agnostic approach to architecture.


Author: Tom Swistak

Tom is a UI Architecture Consultant he contributes to Logic Rooms growing ecosystem of content and provides consultancy services in Mobx, Redux, Dependency Injection and Reactive Architecture

LinkedIn

Want to learn 5 critical lessons for framework-agnostic JavaScript UI app design and structure?

Download our 28-page book exploring big picture ideas to help you create better architecture with any UI framework (inc. React, Angular or Vue).

Download
Close

50% Complete

Book Trial

Please fill out your details if you would like to trial our system.