React by itself doesn’t actually solve how to propagate changes, though. For that, the most popular solution is another Facebook framework called Flux. In Flux, you have stores that contain data, and dispatchers that process actions and notify parties appropriately when a relevant action has been performed. This flow is unidirectional: user actions trigger a dispatcher action, the dispatcher updates the stores, the stores update the views, potentially causing them to re-render. You can see a nice diagram that probably conveys it better than my description. To build a real application, you usually combine lots of React views and lots of Flux dispatchers into a coherent whole, so each piece composes nicely with all the others.
This sounds nice, but whenever I look at the code for websites that use Flux, I’ve just felt…well, weird. I felt like I’d seen this pattern before, that I had stories of that way be dragons1 even though it was all “new,” but I couldn’t quite put my finger on why I felt that way.
Indulge me in a thought experiment. Let’s say we’re not writing a program for the web, but rather for a resource-strapped graphical computing environment. Maybe one of those embedded microcontrollers that are so popular these days, something even less powerful than a Raspberry Pi. How would we design such a framework?
Well, we mentioned resource-strapped, so instead of keeping an off-screen buffer for every single widget like we do on OS X, we’ll instead just have widgets redraw themselves when we need them to. Because of that, we’ll need to mandate that a widget’s drawing code be idempotent. Further, because (again) we’re resource-strapped, we don’t want to lug around a big runtime or anything. To keep things simple, we’ll say that each widget has a single procedure associated with it, and we’ll give that procedure two arguments: an integer representing what action the user just did, and a pointer to some additional action-specific data (if applicable). That way, each widget can handle each message in the most efficient way possible. Finally, because a user action might require us to redraw a widget, we’ll allow the widget to tell us if it needs to be changed. Because drawing is idempotent, we can minimize CPU usage by only redrawing whatever it says we have to redraw. Finally, for programmer sanity, we’ll allow these widgets to nest, and to pass custom messages to each other.
Congratulations! We’ve just designed Windows.
Specifically, Windows 1.0. Circa 1985.
Blasts from the Past
This is, really and truly, exactly how Windows used to be programmed all the way through at least Windows 7, and many modern Windows programs still work this way under the hood.2 Views would be drawn whenever asked via a
WM_PAINT message. To be fast,
WM_PAINT messages generally only used state stored locally in the view,3 and to keep them repeatable, they were forbidden from manipulating state. Because painting was separated from state changes, Windows could redraw only the part of the screen that actually needed to be redrawn. Each view had a function associated with it, called its
WndProc,4 that took four parameters: the actual view getting updated;
uMsg, which was the message type as an integer; and two parameters called
lParam that contained data specific to the message.5 The
WndProc would update any data stores in response to that message (frequently by sending off additional application-specific messages), and then, if applicable, the data stores could mark the relevant part of the screen as invalid, triggering a new paint cycle. Finally, for programmer sanity, you can combine lots of views inside other views. Virtually every widget you see in Windows — every list, every checkbox, every button, every menu — is actually its own little reusable view, each with its own little
WndProc and its own messages.
This is exactly what Flux does. You can see this design very clearly in the official Flux repository’s example app. We can see the messages getting defined as an enum,6 just like you’d do in old-school Windows programming. We can see the giant switch statement over all the messages, and note that different data is extracted based on the message’s
actionType. And, of course, you can see components rendering idempotently based off their small amount of internal state.
Everything old is new again.
When the hurly-burly’s done
Flux clearly works. Not just that; it clearly scales: people and companies, not least among them Facebook itself, are writing huge, functioning applications in this style.
But that shouldn’t be surprising; after all, many companies wrote huge, functioning applications for old versions of Windows, too. Just because it works—and moreover, even if I grant that it works and it’s better than what we had before—doesn’t mean it’s the be-all, end-all of web development.
Much as Windows wasn’t locked into
WndProc-oriented code forever, I’m sure we’re not going to stop here on web development. We’ll rediscover Interface Builder and Morphic. I’ll get my web-based take on VisualAge, Delphi, and VisualBasic. Even if Flux lives on under the hood, I won’t have to think in its terms day-to-day. I know that will happen, because it has happened before, many times, on many platforms. There’s a ton of historical precedent; I just need to wait.
But maybe, with the benefit of hindsight this time, and recognizing that we have just brought the mid-eighties to web development, we can get through this phase a little faster than the last time.
Here’s to hoping.
- Or daemons. [return]
- WinUI programs probably work this way technically at some level, but the lowest level of abstraction for WinUI is thankfully much higher. [return]
GWLP_USERDATA, if you’re curious. [return]
- It’s called
WndProcbecause Windows calls views windows. Indeed, Windows’ concept of “window” is just a little chunk of screen with its own WndProc managing it. OS X’s Quartz actually used to make the same call deep under the hood, though Cocoa obviously exposes a much higher level of abstraction. [return]
- In practice, for all but the simplest of messages,
wParamwas ignored, and
lParampointed at a
structthat contained data specific to that message. This actually makes it even closer to the Flux model. [return]