Sane state management in a sane frontend language (Rescript with React and MobX)

Valde
6 min readOct 23, 2021

--

In this piece we will interest ourselves with a more ergonomic state management framework that works particularly well with MobX.

This post builds upon a previous post which introduces the language and a minimal setup to get started with developing Rescript React apps (https://medium.com/@Valde/rescript-a-modern-minimal-language-for-the-browser-with-react-bindings-169717ce7a0). It also assumes that the reader has a basic understanding of the Rescript language.

Scaling state in Rescript

A problem that is familiar to frontend developers who have worked on larger React projects with large forms, is performance. Once an app grows sufficiently large, spurious re-renders tend to occur more frequently, to a point where the performance degrades UX, like lagging text fields. Using ordinary React state, the props for a text field would usually be a state and a setState prop, among others, like labels and such. Some unfortunate consequences arise form passing state like this.

Some of you might know where this is going now.

The optimization saga starts by wrapping most, if not all components in React.memo. Then you realize that functions do not have a stable address through re-renders, and turn to useCallback. useCallback and, in fact, the whole memo package is notoriously complicated to debug and extend. If you change the function wrapped in useCallback and forget to update the dependency list, then you may encounter very odd bugs like components skipping re-renders they shouldn’t or even closing (as in a closure) in old state which often leads to unintended behavior. One might turn to a reducer pattern, such as useReducer but this suffers similar problems once one wants to compose. Composition occurs very often in Rescript, since it is a functional programming language and is curried by default. Functions are composed and created left and right, so the need to keep an eye on when functions (or objects) change (and how they change) is bug-ridden and not very elegant.

FRP and signal networks

React state is fundamentally unable to solve the re-rendering issue by itself, since React uses a pessimistic model re-rendering (re-rendering as in diffing and updating the dom) when state changes. When a component is either mounted or it has had its state changed, it performs a re-render of itself and all child components (which can be optimized by using memo, but we have already been here).

Instead if we revise what it means to have state change, we might discover a simpler solution. A concept discovered 24 years ago called functional reactive programming can help us discover a simpler, more elegant model [1]. More precisely, push-based FPR models can help us understand an alternative.

In push-based FPR we can model atoms, or rather, our mutable state, via something often named signals. A signal is a discrete stream of values, or in simpler terms, a value that changes over time. We can combine atomic signals into other signals, these combined signal will change whenever any of the signals they consist of, change. We can even combine these signals with other signals into new signals, hence the name “signal network”. The result is something that is similar to how fibers in cooperative multitasking tell the runtime system when they are done, in contrast to the pessimistic model that tells things that they should re-render, which is closer to preemptive multitasking.

Mapping this into React we can, for instance, combine signals into a composite signal which we will name ‘props’ and create a final signal we will call a ‘component’. We can then combine these ‘component’ signals into an entire application, and whenever a ‘component’ signal changes it will automatically update itself. Since any composite signal changes, if any of the signals that it depends upon change. With this model we can derive an application that re-renders exactly when it needs to.

A model called ‘observable’s is awfully close to this FRP signal description, and is luckily something there has been much work on in the frontend ecosystem. A software package called ‘MobX’ has been an interest of mine lately, and has proven to integrate very well into applications written in Rescript with React for a couple of reasons.

MobX in Rescript

Before delving into all the good stuff, let us take a look at an ordinary application written using React state. In this app we will have a component which will harbor the state (as an array) for an array of children. The children can each change their state through a setState callback and view it.

Don’t mind the semantics or naming of name and age, they are just exemplary names for state.

Enabling visual component updates in my browser shows something very unfortunate.

Note the empty input fields, the update is caused programmatically. The input fields are for manual input experimentation.

The solution is non-trivial and involves a lot of memo.

Before using MobX in Rescript, we must write some interop types for the library.

If we now model the previous code, but with MobX, the solution comes out both cleaner and re-renders are constant time, instead of linear.

Even better, we are rewarded, performance wise for decomposing! Say if we were to refactor this app and decompose our components further.

The re-renders come out just as we expect!

A convenient representation of state

In MobX state is “observed” when it is “dotten” into (dotted as in object access notation; a.b.c). This property is very convenient in Rescript, since the standard way of representing state is via ref. ref is compiled to a javascript object that has one field named contents. When we access this contents we also ‘dot’ into the observable. Updates to a ref occur like reassignments, in fact ref(0) := 2 is compiled into {contents: 0}[contents] = 2, which is the type of updates MobX listens to and notifies its observers about.

Modelling composite signals in MobX

We are almost at our FRP model with the aforementioned techniques. The last piece is composite signals. In MobX we have something called a “computed” which has the property of being an observable that only changes when any of the values it observes also change. Say if we were sum the age of every second entry, and only re-render when any of those changed, well, we can!

Consider the difference between changing an odd indexed tuple of name and age

versus an even indexed tuple.

Cleaning up the implementation

The aforementioned application looks a bit noisy, and doesn’t necessarily represent well structured code. We can clean up the final product a bit by moving components into their own files and importing the used modules at the toplevel.

Note the use of Belt (filteri -> keepWithIndex and the order of reduce’s parameters), which is the Rescript standard library.

Conclusion

Like many technologies there are tradeoffs, some more apparent than others. In this instance one would have to commit themselves to learning a new language and technology. In regard to the things I value, this combination is a clear win.

The repository containing the code can be found here https://github.com/ValdemarGr/rescript-post/.

References

[1] http://conal.net/papers/icfp97/

--

--