React with(out) MobX (part 2 of 2)

How to get all the benefits of MobX without “polluting” your stores and components

This post shows another journey I had in the development of timefic.com, from React components with explicit MobX inside components and stores, to React components and stores where there is no single reference to MobX at all, but still, make use of MobX under the hood.

Note: This is the second part of this article.

In the previous article, I told you about the benefits of defining a single hoc (higher order component) for all your application. This component is not really a React component, but a function that returns an enhanced component.

What I am proposing is that you make every component of your app an enhanced component. So, if your app has 250 components you need to enhance 250 components:

A stateless component (named export) and an enhanced component (default export)

You will stop thinking about:

How the data and functions come into the props of your component.

come into the props of your component. If the component is re-rendering when suppose not to .

. And stop needing classes for lifecycle methods.

Your component is not responsible anymore for these “little details”: the hoc is in charge!

So, the Scene 1 was the standard approach (using MobX inside your components and your stores) and Scene 2 was not using explicit MobX, remember? Back to the story now 👇

Scene 2: The “MobX independent” approach

In short: the idea is to use MobX but without referencing it.

The store level

At the store level, using MobX “as usual” made me a little uncomfortable, because:

Each of my stores (17 in the case of the Meetings module) were getting bigger and bigger . So, I created a function for each field in a separate file to transform the store more like an index file.

. So, I created a function for each field in a separate file to transform the store more like an index file. The word this was all around. I need to feed any computed value with the input they needed, as they were now in a separate file.

was all around. I need to feed any computed value with the input they needed, as they were now in a separate file. I had no dependency injection mechanism, so testing the store was difficult (and the decorators all around does not help also).

A typical MobX store with computed fields as functions and no dependency injection

As you see in the image this ActionsStore has 1 observable and 4 computed fields. Each computed field is a function that takes input from the store itself and other stores, in this case: PeopleStore, MeetingsStore, MembersStore and State.

To test this file I needed data inside the mentioned stores, but how can I put data inside them?

The store you are looking at is one of the simple stores I had, so imagine a file of typically 200 lines when you want to look a something that does not work well… 😧

So, I needed to do something about it.

I don’t remember exactly how (I am always reading some functional programming techniques) but it came up a curried function that pointed the way.

A computed field in two flavours: curried and non-curried

Like the image shows, curried version does not need this and the State store to be in the scope of the function. Now self is defining this scope (the scope is the store itself as we will see). There is all the information every field in the store may need (other stores and other fields in the same store).

So, because the function is curried, if you call getMeetingActions(this) you get a function that has exactly the data they need to compute the value.

The strategy emerges:

Every computed field will be obtained calling a function (a curried function), with the store itself (this) as context.

But, we will hide this implementation detail to get stores looking like this:

The new version of the Store: Where is MobX?

Look at the image: where is MobX?

Think about it: If every computed value has the same signature, in this case, is the result of calling the function with this as argument, then is possible to define them as a list of fields inside an object, like the image shows.

Something similar is used for actions, observables and reactions: the basic building block of most of MobX stores.

For example, computed values are implemented like this:

Implementation (with MobX) of a generic computed value

Furthermore, if you want to use another MobX-like reactive state library, you just need to touch this piece of code!

Benefits:

Standard store creation : stores a created with a (factory) function. Need to make every store debuggable? Just add window[StoreName] = this to that factory function. Now you can inspect any store you create in the console (this is very useful).

: stores a created with a (factory) function. Need to make every store debuggable? Just add to that factory function. Now you can inspect any store you create in the console (this is very useful). Reduced boilerplate : compare the store before and after. Not only the latter is smaller but also has 8 computed fields instead of 4.

: compare the store before and after. Not only the latter is smaller but also has 8 computed fields instead of 4. Legibility : isn’t a pleasure to read it also?

: isn’t a pleasure to read it also? Framework agnostic : tired of MobX (I love it!)? Just implement its primitives in other way and you are ready to go!

: tired of MobX (I love it!)? Just implement its primitives in other way and you are ready to go! Testability: You will be testing the function that is returned from the curried function, which means that has all his dependencies already passed as arguments. Also, you can create a new set of consistency tests taking the object inside createStoreDefinition and comparing it to what is actually used outside the store (components or other stores). This may be a future post itself, but keep in mind that a definition not only can be used to “generate code” but also to test it.

To complete the store there is only one detail missing:

Each store needs to be aware of a minimum set of the other stores because inside this has to be a reference to the context that computed fields need.

In other words:

If myMeetingActions needs the value of State[‘App_meeting_id’], then State store must be used to construct ActionsStore.

That means that can’t be circular dependencies.

If Store C needs Store A and Store B, then Store B can’t depend on Store C. In case that happens (it does) you need to break Store C, for example, in two stores. This makes sense when you are forced to do it because you realized there were things interleaved (no well designed).

For example, here are a list of stores in timefic:

The creation of the Stores: Order matters — stores cannot have circular dependencies

Last benefit?

Looking at this files is really easy to think about how the data is being composed.

That’s it. I hope this makes sense and can be helpful!