A different way to manage state in React

David Gilbertson
Oct 30, 2018 · 13 min read

Imagine a React app that shows a list of tasks.

In this app, there is a component called <TaskList> that renders a bunch of <Task> components, and a button to add a new task.

The <Task> component is this masterpiece:

Features include: ticking a box to mark a task as done, typing to change a task’s name, and deleting a task.

Wowo!

One last code block before I get to a rambling intro and address the massive flaws you’ve seen so far…

In this one, I add some data to the store, then render the <TaskList> into the DOM.

That’s the whole app.

There’s no Redux, no context, no providers or consumers. No reducers operating on immutable stores, no component state, just a global store object being read from and written to.

Hopefully, upon seeing this you’re thinking “that won’t work”, and “who gave this guy a keyboard”, because that means you might find the rest of this page interesting.

Or if you’ve got a little more faith in me, you might be thinking “what’s react-recollect? What does that collect function do? How can that store object possibly work?”, which I will get to very, very soon.

Relevant

If you’d like to play along, here’s a code sandbox. If you’re the readme-reading type, jump right in, my friend. If you’d rather peruse the source of an app that’s actually using this, knock yourself out.

For the rest of you here is the …

The long awaited intro

Question: why is React great?

Answer: because it changed how we think about applications. We now write apps by defining:

  • “when the data looks like A, the app should look like B”, and
  • “when the user does X, change the data like Y”.

We no longer have to write super brittle code like: “when the user does X, update the app to look like B”, which was invariably fiddly because you didn’t necessarily know what the previous state was.

And whaddaya know, this meant writing apps became so, so much faster. And more pleasant to boot. At least once a week I find myself whispering into my laptop’s microphone I love you React.

But there’s something going on in the application of this paradigm that I would like to address.

I mean, we don’t just say “when the data looks like A …”. Because first we need to wrap the top level component in some provider, and then connect components via mapStateToProps and declare (in advance!) what data we’ll need in that component, and pass down countless props to child components after a 20 minute discussion about whether one particular component should be connected to the store directly or receive props from its parent since the parent already had that prop and had to pass down some others anyway.

And we don’t actually say “when the user does X, change the data like Y”. We say “when the user does X, call an action creator that was passed in as a prop via a mapDispatchToProps function and it will dispatch an action with a particular type which will be caught in a switch/case statement in one or more reducers which will update the store like Y”.

I certainly don’t want to pick on Redux, because I’m a fan of both the library and the the creator, but, I mean, check out the docs for a basic todo list. That’s 13 files. Don’t read it on a Friday.

And don’t even get me started about asynchronous actions. I’d rather take a BBQ sauce bath in a cannibal colony than spend another minute with redux-saga. How did we get to the point where updating a JavaScript object requires a tool with 20 pages of documentation?

Are we slowly being boiled alive?

In BBQ sauce?


But all this complexity isn’t just there for shits and giggles, it is necessary. It’s necessary because of the way that React works.

When looked at from a particular angle, you could say that React uses trial and error when updating the page. It asks “if I re-rendered this component, would the output be different from the last time? No? What about this one? Yes? And this one?”, and so on.

What if React no longer needed to do this? What if React knew what it needed to update?

Could we then remove a lot of this complexity?


Before moving on, let me state that I don’t think what I’ve made is really useful in the real world. Redux will continue to be my go-to state management tool for any production project, and I totally get the reason for all the moving parts and am not a boilerplate-hater.

But my inner salesman can’t help but ‘pitch’ this thing I’ve made, so here we go…

The gist of it

What if some central entity had a view of your entire application and knew the relationships between every property in your store and every component on your page?

When a property in the store changed, this entity could directly instruct React: “hey, update these two components”.

That’s what I set out to build with ‘Recollect’ (which in hindsight I should have called Panopticon).

There’s three pieces to the puzzle:

  1. I wrap the store (a plain old JavaScript object) in a Proxy object, so I can watch as data is read from the store.
  2. I wrap each component in a Higher Order Component, so I can ‘record’ what’s being read from the store as the component renders.
  3. Since the store is wrapped in a Proxy, I can also see when data is written to it, and respond by updating any component that read that same data when it last rendered.

The lifecycle

Let’s look at the lifecycle of one user interaction.

First we must define: When the data looks like A, the app should look like B.

Concretely: “if a task is done, display a checkbox with a tick in it. Otherwise don’t show the tick”.

Let’s start in the parent <TaskList> component. We’ll import the store object and loop over store.tasks, rendering a <Task> component for each one, passing in the task as a prop.

Since task is an object, we’re just passing around a reference. So when we refer to it down in the <Task> component, we’re referring to a place in the store.

Let’s zoom in on just the checkbox that shows if a task is done.

This component reads task.done while rendering; Recollect will see this and record an association between that property and this component. (It’s irrelevant that you don’t see the store object anywhere in this component.)

Next: When the user does X, change the data like Y.

Specifically: we want to respond to a click on the checkbox by changing the value of task.done.

task.done = e.target.checked is all we need for that. Pretty simple, hey? When I first got this working I did a lot of happy swearing.

When the user has clicked, and the store has been updated, Recollect takes over. It looks through its list of all the components that use this done property and instructs React to re-render them.

So, lines 6 and 7 in the code above represent the whole render cycle.

Component instances

Here’s a neat (maybe obvious) thing, Recollect associates component instances with properties. So if you have five tasks, then the third <Task> component will be associated with the done property of the third task. When you tick ‘done’ for the third task, only that third <Task> component will be updated.

The parent component doesn’t even know that this task was updated since we’re not creating a whole new array.

Now, suppose you had a <Summary> component that showed a count of incomplete tasks, perhaps with the logic:

store.tasks.filter(task => !task.done).length

Recollect will know that this component also read the done property of the third task when rendering (it read the done property of all tasks). So it will get an update too.

The rest of the app (which has nothing to do with this third task being done) doesn’t get touched. Not even the children of updated components will be re-rendered, unless Recollect says they should be.

Adding/removing from an array

Let’s take a closer look at the <TaskList> component. What if someone adds a task?

Well, because this component read an array when rendering — store.tasks.map — Recollect knows that if the length property of that array changes, then the <TaskList> should be re-rendered (to either add or remove items). The <TaskList> doesn’t need to know if the contents of the array changes.

Conditional rendering

Now imagine if a component’s render method did something like:

if (!store.tasks.length) return null;return <p>The first thing to do is {store.tasks[0].name}</p>

On the first render, if there are no tasks, Recollect will record that this component only needs to know about changes to the length of the tasks array.

If that array length changes, then it will re-render the component, at which point, the component would render the name of the first task, which Recollect would make a note of, and then update later if required.

So, for each re-render, Recollect discards its record of whatever the component needed on the previous render and records again from scratch.


To recap, when you want to read from the store, you do store.foo. When you want to write to the store, you do store.foo = 'bar'.

Recollect looks after everything else.


I think a nice side effect of this simplicity is that Recollect doesn’t tell you how to abstract your logic for the best scalability. If you wanna do pub/sub, do that. If you wanna write a bunch of functions like updateTask() and markTaskAsDone() and deleteTask() and stick ‘em all in a single file then you can do that.

Whatever makes you happy.

If you want to use generators somehow to fetch data and stick it in the store, then I question your sanity but suspect it would be prudent to not confront you about it.

My suggestion would be to update the store directly from the components in most cases, until you’ve got a reason not to.

Being able to look in the onChange callback for a button and see what it changes, and see that it doesn’t call any other functions, means that you know exactly what’s happening.

You know “if the user does X, change the data like Y”.


Speaking of easy debugging…

I’ve exposed a few things globally in window.__RR__ so you can get your fingers a bit dirty.

You can turn on debugging with __RR__.debugOn(), and get a reference to the store with __RR__.getStore().

As you change the store, Recollect (who is always watching), will instruct React to update the interface accordingly.

To reiterate, it doesn’t matter where you change the store from, the UI will always reflect the current state of the data, and that includes changes made in the console.

There’s something pleasing about typing this and seeing the little checkbox get checked.

You can even write snippets in the Chrome DevTools if you want to simulate more complex things, or want to build out a whole bunch of mock data:

It works in production too, which might be useful for 7 minutes a year.

Lastly, you can subscribe to changes in the store, and — you guessed it — from anywhere in your app.

This example loads data from local storage before rendering, then subscribes to changes in the store to keep local storage up to date:

The nicest thing I’ve found is that asynchronous tasks require no special treatment. (You can write to or read from the store anywhere, at any time, did I mention?)

You can fetch a bunch of tasks from the network, wait till the clock strikes twelve, make a sandwich, then do store.tasks = data.

Recollect will say “ooh, I’ve got new tasks, let me update these six components in particular”.


Alrighty! Thank you for sitting through my sales presentation that got pretty full on toward the end there. You will get your speed boat shortly, but right now we must talk about …

Problems

It’s so ingrained in me that immutability is good and mutating state is bad that I can’t quite recall if this is true only in the case of React/Redux.

Part of my brain freaks out when changing the store directly, but why? And after answering ‘why’, (and ‘why’ five more times) I think the answer is always “so React knows when to update”.

So since we can tell React when to update, immutability is just a nuisance, right?

I’m open to being corrected.

Edit: v2 of Recollect is out and it’s now immutable, but the immutability is invisible. I elaborate in the readme.

As for ‘mixing of concerns’ by updating the store in the UI layer, I think that’s a sort of denial. If you’re calling an action creator from the UI, you’re still updating the store from the UI layer, just not directly.

The same goes for the concern that “functions shouldn’t have side effects”. If a user clicks a button, and the store should update as a result, then that function has a side effect, no matter which way you slice it.


Onto the more serious problems. There’s the big fat obvious one that this uses Proxies, so no IE11, no Safari 9, and no Blackberry Browser! How will people that work in the payroll department of federal government agencies use your website!

I think maybe you could get it to work without proxies using some fancy footwork and constructing objects with getters and setters. But not in 160 lines of code and not in one weekend. So I’ll leave that to someone else.

(See how I classy-bragged that I made this in a weekend? That’s why they call me the smooth-meister.)

There’s also risk of confusion here. If you pass a task object to a child component, then it’s a reference to the object in the store and you can update it, but if you pass the task name, then it isn’t a reference and you can’t update it.

I wonder if — in a large app with developers of different skill levels — this would become frustrating, or something you’d get used to.

And, is it a bad thing that you can access the store from anywhere? The voice in the back of my head says “gee it’s dark in here” but also “restricting things is good”, but I can’t put my finger on why.

Ah, and also I’ve done very little testing. I’ve tested it in Chrome and Chrome and nothing else. But I did convert my current side project to use Recollect. It’s quite data heavy, has thousands of DOM nodes, and is purring like a kitten.

Also, this relies on React rendering in a particular way for Recollect to be able to ‘record’ components retrieving data from the store. If React version 17 were to change that for some reason, this could all unravel like a ball of wool being attacked by the kitten from the previous paragraph.

Performance

This was a bit disappointing. I expected this to be heaps faster than React with Redux, or even React with component state.

But in my tests, Recollect and plain old setState on a parent component were about as fast as each other. To be fair though, even with 2,000 DOM nodes, they were both updating in 40ms. So I clearly need a bigger app to test on.

I suspect that the bigger the app, the bigger the performance gain you’ll get from Recollect. But then I would say that, wouldn’t I.

File size is an interesting one. It depends on the app, because if you’re replacing hundreds of lines of reducers with this tiny thing, your file size will probably go down. In my relatively small app (that didn’t have Redux), my total JavaScript size went from 38.9 KB to 39.3 KB after adding react-recollect and removing all the reducer-type logic I had.

So if you wanted to come up with a particular number, you could say that Recollect is something like 1 KB. But really, it’s likely to make your app smaller.

So, what next?

I’m still riddled with self doubt about this. I’m quite certain I’m not the first person to come up with this approach, so I figure one of the following must be true:

  • The fact that browser support is less than 90% means it’s of no interest to the real world, so no one’s ever pursued the idea.
  • The idea has some some serious problem I haven’t thought of.
  • It has been done, it’s a popular library, and I haven’t come across it because my narrow mind dismisses anything that isn’t React+Redux.

And I purposefully haven’t looked for tools that do this; I’m more interested in exploring than being a productive member of society.

Yet the dread of prior art looms large. If you’re aware of a library that did all this four years ago, you could play a fun prank on me by not telling me about it in front of all my internet friends.

Edit, one day later: “Congratulations, dickhead, you just re-invented MobX”.

Although to be fair (to me), it’s not exactly the same. With MobX it seems like you have to say in advance which props you plan on using in a component and define them as ‘observables’. I’m sure there’s a good reason for this, but as an API minimalist, I like the idea that with Recollect you don’t need to worry your pretty little head about concepts such as observables, it all gets worked out behind the scenes.

Edit 2: react-easy-state is super similar to what I’ve done. I think (for better or worse) I’ve gone one step simpler and there’s no need to ‘wrap’ an object to create a store. Also their internals are very different to mine, which has me thinking there’s something I’m missing. I’ll keep digging…


Despite these reservations, I will most definitely default to using this on personal projects. It saves so much time and lets me focus on application logic.

As for production projects, I’ll let you lot try it out for me first :)

That’s all folks

Thanks as always for reading!

I hope that you see a tree today and think that’s a nice tree and have a little moment of joy.

HackerNoon.com

how hackers start their afternoons.

David Gilbertson

Written by

I like web stuff.

HackerNoon.com

how hackers start their afternoons.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade