The Case for Flux
Flux has become trendy in the past few months, and you’ve seen everyone and their dog roll out their own implementations. The momentum around this pattern is so hard to ignore that, if you are a React beginner, you might be in a position where you think that Flux is the way to write React apps, but you’re frustrated that you don’t get it.
This is a problem. It is in React’s spirit to constantly question the dogma, and we must be careful not to introduce a dogma of our own. As a community, we need to teach the beginners to keep their eyes wide open so that they can recognize it when something better than Flux comes along.
Friends don’t let friends pick a design pattern blindly.
If you used Microsoft stack in 00's, you remember the dominant NIM mentality: Not Invented at Microsoft. Nobody would use an ORM unless it came from the blessed vendor.
The web community as a whole is far from this thinking but we must still be alert because no dogma starts out as a dogma.
A dogma is born when the solutions are presented without enough original context, and newcomers feel pressured to delegate crucial decisions to an authority.
This might feel reassuring at first, but you can’t escape the nagging suspicion that the choice might have been wrong. This suspicion will fuel you to either ask questions (net win), abandon the solution without understanding when it’s applicable (loss for you), or to spread the dogma (or publicly speak against it) sans the context (net loss).
As the time passes and the context is forgotten, dogma stays. Acolytes form a bubble to protect themselves from fear of what they didn’t learn in the past, evaluation of the choices they make in the present and irrelevance of their dogma in the future. What a way to waste everyone’s time!
Rest assured, you’re not stupid. Sometimes we suck at explanations, and you’re well within your rights to demand better ones. As an industry, we must try harder at being inclusive, and inclusiveness means less ego, petty camps and slogans, and more mentorship, stealing good ideas and knowledge distillation.
It’s our generation’s fault if today’s good ideas are forgotten and rediscovered again in thirty years because nobody made them accessible.
Say you’re a beginner and you’re choosing React for your first webapp. You might not even know about the challenges in building the webapp, let alone the solutions! This, combined with Flux’s verbosity and perceived complexity in small projects, will likely drive you nuts.
It’s tricky to understand what problems Flux is supposed to solve if you haven’t tried to solve them yourself before. I’ve seen people take the worst and leave the best from Flux due to not understanding the tradeoffs, and even giving up on React because of Flux. Ouch!
The trouble is, humans do have a knack of choosing precisely those things that are worst for them.
If you’re a diehard fan of some technology, it’s not music to your ears, but people rarely speak up if they can’t grok your toy. More often they’d just feel stupid and disappointed, regret the wasted time and silently move on.
When I first read about Flux, I knew we had to migrate Stampsy to it as soon as possible, even at the cost of more than a month of no new features. That’s what we did, and it paid off in maintenance and the ease of adding new features later.
I was lucky. Our moderately complex Backbone app was killing me with precisely the same problems that Flux set out to solve, so the motivation behind it clicked for me very quickly. I would have probably missed it if I hadn’t been fighting with Backbone so hard.
Below I outline a few symptoms that, when exhibited by your app, justify the boilerplate and learning curve added by Flux.
If your app doesn’t have these symptoms and is not likely to develop them in the near future, stay with what works for you.
On the contrary, if this charade seems familiar, you might have a case for Flux.
I’m going to use Backbone as an example of a non-Flux approach to data handling, but my points equally apply to any traditional MVC framework with models and collections.
You Might Need Flux…
… if your data changes over time
Flux can work for any kind of application, but it won’t make as much sense to you if you’re just rendering static data. Flux is only going to feel unnatural and convoluted. Remember, the very point of Flux is to make data changes easy to reason about. (Bonus points if changes need to be persisted somewhere, for example to your server.)
Another important point I want to stress is that you don’t need Flux if you don’t care about immediately reflecting changes in the UI. If you’re okay with data appearing out of sync until the next refresh, stay with the tools and patterns you already know and like.
… if you want to cache data in memory, but it can change while cached
Once you have fetched the data, you might want to keep it around. For example, if you show a list of items and user can open a detail page for each item, you want to cache the list, so that scroll position is preserved when user presses Back.
This is already getting tricky because you can’t just cache the server response and reuse it. What if the user changes an item while on the detail page, and then goes back? You’ll want to display the cached list, but updated with the new data. For example, a single article’s title may have changed.
This calls for a global entity cache and caching IDs of items on the page instead of the actual JSON from server. If you’re using Backbone collections, you have to jump through insane hoops at this point because they will try to create model objects for each server response, but you want them to reuse (and potentially update) objects from the cache. It’s doable but gets ugly soon.
We’ll also probably want to populate the cache from JSON supplied by server at the page load, so that the page doesn’t load with a spinner.
… if your data is relational and models include and depend on each other
Say we’re writing something a bit like Medium, and each post has an author. Each author has a published post counter on their profile page. When a new post is published, that counter must be incremented. That’s trivial with normal MVC, where’s the need for Flux?
Say users can follow each other, and when following succeeds, we need to update “followers” counter of one user and “following” counter of another user, as well as “is following” and “is follower” boolean fields to re-render the button state.
We want to do this optimistically and without waiting for server response. This means we first increment the counters and update boolean flags, then send a request to server, and if it fails, we roll back both the counters and the flags. Simple huh?
We can implement this as a method on a model, but this means one of them will be managed by another one, as in user.followUser(otherUser). Such indirection makes bugs trickier to track, and it only gets worse if entities of different types need to be updated and rolled back together.
Don’t forget that we want caching too, as discussed earlier. What if we have already cached user’s “following” page so they can go Back without resetting scroll? This cache would become stale if they “follow” somebody else. We need to find a way to update/invalidate that cache. Same goes for that person’s “followed by” list.
One way to fix this is to invalidate this cache so it is re-fetched immediately after user presses Back and scroll position is restored. Another way to fix this is to avoid another network request and preemptively insert a new model into the cached list, since we already know what happened. Either way, at this point your day job is solely fighting with Backbone.
But wait, there’s more. Each of these lists is paginated. (Backbone collections can’t do that on their own, but there are plugins.) What if you want to add pagination in the other direction a la Twitter’s “there’s 3 new tweets” feature? You’ll need to go out of your way to implement this if your model framework doesn’t already support this out of the box.
… if the same data is assembled from different sources and can be rendered in several places throughout the UI
Say, an article can be liked by people (that happens). You’ll want to show first several likes in the article’s footer and fetch them in the same JSON response as the article itself so they’re displayed immediately. Fine, there go your first three likes.
But you’ll also want to have a “more” button that opens a sidebar with a list of all users that have ever liked your article. It will use a different API endpoint, and it will be paginated.
If you press “like”, you’ll want to update both “first several likes” list in the UI, as well as the “all paginated likes in a sidebar” list.
It would be nice if those two lists corresponded to the same model collection, but this means the same model collection needs to be fed from two different API responses. Turns out, 1:1 correspondence between API responses and model objects doesn’t scale! How do you like this, Backbone?
The Flux solution: No fat models, separate writing and reading as it gets more sophisticated
There is a simple solution to these problems: don’t use fat models. Let them be plain objects and don’t try to shoehorn data reading and aggregation into them.
Another important lesson is that data from server is not your model. It serves as yet another input, but it can’t be the primary model, just like it’s hard to express complex domain level logic with classes generated by an ORM.
Flux isolates all data mutations to a particular layer in the application and establishes a completely predictable way to get data in and out of there.
This is nothing new, it’s called Command Query Responsibility Segregation:
Do you recognize the Flux Overview diagram in disguise?
Caching, invalidation, optimistic updates, aggregation, pagination and a lot of other things get much easier when models are plain objects and don’t try to manage complex updates of each other.
If you are serious about working with data, there has to be a single source of truth for all of it. Neither the UI nor other models should be able to mutate the data.
In Flux, Store is the only place in your whole app that has privilege to mutate the data. It has no setters and only responds to actions emitted by the components. API responses are also actions, as they serve as inputs to Store. Only Store gets to decide how to update the data.
When the data is wrong, you can trace exactly where the corruption occurred, because you know 100% it happened in the Store.
This was an overview of the problems Flux helps to solve.
Flux requires you to write more boilerplate code, but in return it makes it easy to reason about complex data changes, and opens up a lot of possibilities impossible with classic MVC, such as recording and replaying UI state just by re-dispatching serialized actions.
Flux also frees you from the limitations of a particular MVC implementation because you manage your own data.
If your app doesn’t have complex data changes and caching, don’t use it.
But if it does, I strongly recommend you try Flux.
Published in #SWLH (Startups, Wanderlust, and Life Hacking)