React Easy State 6

A story of compromises

Easy State is a minimalistic Proxy based React state management tool, with a strong focus on practicality instead of theoretical beauty. It was born with this philosophy and it will keep to it until the end of its time. I am careful to not pollute it with unnecessary features; in fact, I think it is feature complete for more than a year now. From time to time I have to tweak a few things to keep up with the trends and align with your feedback though. Version 6 does exactly this with two small changes. Both took minutes to implement but forced me into long frustrated brainstorming sessions beforehand. In the end, I gave in and accepted that sometimes compromises are unavoidable.

Image by Noppakaw

Batching

Re-rendering something multiple times between two paint events hurts performance and has no benefits. This why most frameworks have some render batching logic.

React’s state updates are naturally batched by setState so it can afford to not worry about this much. People rarely call two setStates in succession but it still has a basic batching mechanism. Multiple setState calls in a single event handler will not cause multiple renders, anywhere else they would though. This is a form of synchronous batching, which has the benefit of transparency in exchange for a limited scope.

It has no visible artifacts from the app developer’s point of view, but to implement it the framework developer must have full control over the source of the state change. The framework has to choke renders before the state mutating code executes and release them at once after it finishes. React is calling event handlers itself, so it has a nice control over how they execute. It simply stops renders before it calls the event handler and resumes them after it finishes. On the other hand, it has zero control over many other task sources, like timers and network events. Calling setState two times as a response to some data from an API will cause two renders because React cannot reach and batch that code.

There are ways to lift this limitation, but it has a hefty price of losing transparency. Async batching collects renders for whole call stacks by stopping re-renders on the first state mutation and resuming them — with requestAnimationFrame or Promises — after some time passed. This method does not care about the task source, but it adds a layer of asynchronicity. From the user’s point of view, nothing will change, but the app developer has to be careful to avoid false assumptions. Take a look at Vue’s testing docs for a great real-life example.

The benefits of sync and async batching can be combined by monkey patching all possible task sources to gain control of the state mutation snippets. This method keeps the transparency of sync batching without narrowing the scope to a few task sources only. Monkey patching feels dirty, but it has a working large-scale example — Angular.

I don’t know which direction React is heading, but I suspect it will join Vue with async batching. In the meantime though, I am forced to go with monkey patching. Easy State does not have the comfort of natural setState batching and it can not afford to put an async layer on top of its current sync batching. Hence I patched the most common task sources with a batching logic in version 6 to avoid unnecessary renders. In practice, it means something like this.

import { store, view } from 'react-easy-state'



const clock = store({ time: new Date() }) // this will cause only one render per second, right after the callback function finished running

setInterval(() => {

clock.time = new Date()

clock.time = new Date()

}, 1000)



const ClockView = view(() => <div>clock.time.toString()</div>)

When React releases its new async batching behavior, Easy State’s own batching will be deprecated and removed in its favor.

Defaulting to ES6

Easy State is based on none polyfillable ES6 Proxies, but I still had to publish it in a transpiled ES5 form for a whole year. Pretty silly.

Nowadays a front-end developer’s life is not only about coding and putting it on the internet. Inbetween the two they run build processes with bundling, transpilation and minification. If any of these tools don’t support ES6 the build process fails and the code never reaches the internet, even though the target browsers would support it. In this case the weak link was uglifyJS, a minifier. It is widely used and it was pretty late to catch up to the other tools in terms of ES6, which forced me to distribute the none ES5 compatible Easy State in a transpiled ES5 bundle. The two bundles have the same functionality and roughly same performance so sticking to ES5 should be okay. But it isn’t … there is an incompatibility between ES5 and ES6 classes.

Let’s say that you would like to create a new app with ES6 and above, which isn’t uncommon nowadays. You would import the transpiled ES5 Easy State package and use it in your ES6 code, each having its own old-school or new class implementation. The problem kicks in when you try to extend an ES5 class by an ES6 one.

In this case, the mandatory super() call breaks the application. I think this issue will start to pop up more often when ES6 only apps become mainstream and research led me to the conclusion that this can not be fixed or correctly transpiled. Leave a comment if you think you can solve this issue!

Long story short, I have to decide between breaking the experience for my forward thinking ES6 users and the ones with old toolchains. My best bet was to monitor the user base and the state of front-end toolchains and decide when to switch. Version 6 finally defaults to the ES6 build while keeping an option to manually chose the old ES5 build when it is necessary.

Conclusion and the future

I tend to find myself in these imperfect situations more and more often, and I hope sharing them helps others to make quick decisions instead of endless frustrating brain meltdowns. Sometimes I also need this push to let things go.

For more than half a year now I am working on a new React library, which gives Proxies some new exciting use. I was blocked by some tough decisions, but hopefully, I can bring you some exciting news in the not so far future.

Thanks for reading!