Front-end developers often refer to transparent reactivity — at the core of MobX, Vue or React Easy State — as magic, but there is nothing magical about it. It is based on a very simple idea, that I will demonstrate with the following snippet and some common sense.
You can perfectly define when you expect
Note to re-render: when a new note is added or removed and when the author or a note’s text is modified. Luckily this conclusion was not driven by complex human intuition, but simple programmable if-else logic.
If a part of a state store — which is used inside a component’s render— mutates, re-render the component to reflect the new state.
You brain is creating the following ternary relations about properties of objects — used inside render methods.
When a property of an object is modified it looks up all of the components, which belong to that
(object, property) pair and re-renders them — imaginarily. Let’s do this in real life!
The rest of the article assumes you have a basic understanding of ES6 Proxies and React Easy State. If you don’t know what I am talking about, a quick look at the MDN Proxy docs and my Introducing React Easy State article is enough to go on.
Making a Reactive Core
In order to construct the
(object, property, component) relations, we have to know which objects and properties do
Note use during their renders. A developer can tell this by a glance at the code, but a library can not.
We also need to know when a property of an object is mutated, to collect the related components from the saved relations and render them.
Both of these can be solved with ES6 Proxies.
store Proxy intercepts all property get and set operations and — respectively — builds and queries the relationship table.
There is one big question remaining: what is
currentlyRenderingComp in the get trap and how do we know which component is rendering at the moment? This is where
view comes into play.
view wraps a component and instruments its
render method with a simple logic. It sets the
currentlyRenderingComp flag to the component while it is rendering. This way we have all the required information to build the relations in our get traps.
property are coming from the trap arguments and
component is the
currentlyRenderingComp — set by
Let’s get back to the notes app and see what happens in the reactive code.
NotesApprenders for the first time.
NotesAppcomponent while it is rendering.
notesarray and renders a
Notefor each note.
- The Proxy around
notesintercepts all get operations and saves the fact that
notes.lengthto render. It creates a
(notes, length, NotesApp)relation.
- The user adds a new note, which changes
- Our reactive core looks up all components in relation with
(notes, length)and re-renders them.
- In our case:
The Real Challenges
The above section shows you how to make an optimistic reactive core, but the real challenge is in the numerous pitfalls, edge cases and design decisions. In this section I will briefly describe some of them.
Scheduling the Renders
A transparent reactivity library should not do anything other than constructing, saving, querying and cleaning up those
(object, property, component) relations on relevant get/set operations. Executing the renders is not part of the job.
Easy State collects stale components on property mutations and passes their renders to a scheduler function. The scheduler can then decide when and how to render them. In our case the scheduler is a dummy
setState , which tells React: ‘I want to be rendered, do it when you feel like it’.
Some reactivity libraries do not have the flexibility of custom schedulers and call
forceUpdate instead of
setState , which translates to: ‘Render me now! I don’t care about your priorities’.
Saving and querying ternary relations is not so difficult. At least I thought so, until I had to clean up after myself.
If a store object or a component is no longer used, all of their relations have to be cleaned up. This requires some cross references — as the relations have to be queryable by
component , by
object and by
(object, property) pairs. Long story short, I messed up and the reactive core behind Easy State leaked memory for a solid year.
After numerous ‘clever’ ways of solving this, I settled with wiping every relation of a component before all of its renders. The relations would then build up again from the triggered get traps — during the render.
This might seem like an overkill, but it had a surprisingly low performance impact and two huge benefits.
- I finally fixed the memory leak.
- Easy State became adaptive to render functions. It dynamically un-observes and re-observes conditional branches — based on the current application state.
Car is not — needlessly—re-rendered on
speed changes, when
car.isMoving is false.
Implementing the Proxy Traps
- Get-like operations retrieve data from an object. These include enumeration, iteration and simple property get/has operations. The
(object, property, component)relations are saved inside their interceptors.
- Set-like operations mutate data. These include property add, set and delete operations and their interceptors query the relationship table for stale components.
- What is a property descriptor?
- Do property set operations traverse the prototype chain?
- Can you delete property accessors with the
- What is the difference between the target and the receiver of a get operation? When can they be different?
- Is there a way to intercept object enumeration?
Managing a Dynamic Store Tree
So far you have seen that
store wraps objects with reactive Proxies, but that only results in one level of reactive properties. Why does the below app re-render when
person.name.first is changed?
To support nested properties the ‘get part’ of our reactive core has to be slightly modified.
The most important section is line 15–18 and it needs some explanation.
- It makes properties reactive lazily — at any depth — by wrapping nested objects in reactive stores at get time.
- It only wraps objects, if they are used inside a component’s render — thanks to the
currentlyRenderingCompcheck. Other objects could never trigger renders and don’t need reactive instrumentation.
- Objects with a cached reactive wrapper are certainly used inside component renders, since the
currentlyRenderingCompcheck— at line 15 — passed for them previously. These objects may trigger a reactive render with property mutation, so the get trap has to return their wrapped versions.
These points— and the fact that relations are cleaned up before every render — results in a minimal, adaptive subset of nested reactive store properties.
Monkey Patching Built-in Objects
this value. If someone calls them with an unexpected
this , they fail with an
incompatible receiver error .
Unfortunately Proxies are also invalid receivers in these cases and Proxy wrapped objects throw the same error.
To work around this, I had to find a viable alternative to Proxies for built-in objects. Luckily they all have a function based interface, so I could resort to old fashioned monkey patching.
The process is very similar to the Proxy based approach. The built-in’s interface has to be split in two groups: set-like and get-like operations. Then the object’s methods has to be patched with the appropriate reactivity logic — namely constructing and querying the reactive relations.
A Bit of Intuition
I was a bit overgeneralizing when I stated that the reactive core is made with cold logic only. In the end I had to use some intuition too.
Making everything reactive is a nice challenge, but goes against user expectations. I collected some meta operations — that people don’t want to be reactive — and left them out of them fun.
These choices were made by intuition during my usage test rounds. Others might have a different approach to this, but I think I collected a sensible subset of the language. Every single operation in the above table has a good reason not to be reactive.
The reactive core — implemented in this article — is not in the source of React Easy State. In reality the reactive logic is in a more general library — called the Observer Utility — and Easy State is just a thin port for React. I intentionally simplified this to make it more digestible, but the presented ideas are still the same. I hope you learnt something new if you made it so far!
If this article captured your interest please help by sharing it. Also check out the Easy State repo and leave a star before you go.