Clojure(Script) is a delightful language and learning it will make you look at familiar concepts in a completely different way. Its emphasis on data transformations gives you immense clarity in understanding a problem: code syntax fades away when you think about how the data flows through the system.
I recently started working on vanilla React projects, and it surprised me that some hard-earned lessons in the cljs ecosystem that I take for granted have not reached the wider React community. ClojureScript and React have been working together really well since the beginning, and cljs devs have scaled very complex UIs thanks to some tricks that I think will benefit everyone growing a React application.
Let's have a look at an example shall we, it will be easier to explain the concepts with some code running in front of your eyes. You want to create a simple component that fetches the Github repos for a given username. You open up your editor and quickly come up with something like this (well, probably looking prettier than this)
What's the problem with it? Well, nothing really. A strong selling point of React is how well it adapts to different styles, whether functional or more object oriented. Here we just want to throw something together and seeing that it works, and the problem is simple enough that we don't need anything more complex that a few stateful components.
But let's imagine for a moment that we're working on a bigger app, and, while still using the same example for simplicity's sake, we want to scale things up a bit. Let's hear what advice we can take from cljs.
🗃️ (1) Keep the state all in one place
That is probably the first thing you're going to hear from any Clojure developer. Stateful components are notoriously harder to understand, debug and scale, precisely because they hide complexity inside them.
Here's David Nolen, author of the hugely influential library om.
And if you're not convinced yet, please do watch Rich Hickey's The value of values.
Imagining the application state as one big "memory database" is what allowed concepts like Pure Components, Global Undo, and Time Travel Debugging to become popular.
Centralising app states greatly simplifies things. Think about it: would you prefer to connect to one database or a hundred different databases?
We also start to see the idea of your components being nothing more than a function that maps data to UI.
Component(data) => UI
In many React circles this idea quickly became popular and that's how Redux came to be (inspired by Flux and The Elm Architecture) and is now part of React itself with the useReducer
hook.
We can rewrite our simple example like so:
This also has the benefit of making the UI more modular: we can extract a Form
component from the previous example because it can just use and update the state independently of other components.
🌶 (2) Make your app Hot Reloadable
Years ago I used to build Android Apps and the OS had this mechanism called onSaveInstanceState()
where Android asks every little piece of stateful UI: "hey, what's your current state?" so that it can save it temporarily when your app is killed by the system. It is quite an annoying thing to implement. I even wrote a library for it.
Imagine how easy it would be for Android if the state of your app was all in one place. Implementing save & restore would be one line of code. And that's why ClojureScript had the first and still the best experience of Hot Module Reload that anyone has ever seen.
Hot Module Reload is not just about updating a function or two: it's about changing the UI behaviour but preserving its state. Watch the talk and see what happens when Bruce Hauman changes the game logic as he plays it: if it doesn't blow your mind, nothing ever will. Oh, and once you recover from the shock go ahead and watch Bret Victor's Inventing on principle for other inspiring ideas. You'll never go back to the old ways of working.
If you want to tweak the RepoItem
view that we have in our example, every time you make a change you'll need to fetch the repositories again, and you'll quickly hit Github rate limits. Why can't the UI stay on the RepoItem
page while you work on it?
There are some solutions in React that enable a similar experience, but they're not quite there yet, and it's definitely not beginner friendly. Instead, every cljs project has hot reload enabled from the start.
We can hack something together quite easily if we write to local storage every time the state changes, and then read it from the storage on reload.
Whatever strategy you use, I strongly suggest you have something, anything, in place that enables HMR without losing state, so that you can experience the joy of live reloading.
🎯 (3) Surgically update components
Some of you might complain that passing the entire state down to the exact component that needs it takes a lot of work. And you will be right. It also forces all the root components to re-render whenever a leaf changes its properties.
That's why all cljs libraries tried to come up with a solution that makes this approach more efficient: om had cursors, reagent had reactive atoms.
A real breakthrough came with the introduction of re-frame, and with the best README you'll ever see.
You want your components to react only to specific changes of the "memory database". The concept is similar to Materialised Views: they represent a particular view of the underlying database and they update only when the data they're interested in changes.
I can't find a talk from Mike Thompson, author of re-frame, but he convinced all of us to watch this video, and you should probably watch it too.
Recently, Recoil introduced similar concepts to most React users and its design is heavily inspired by re-frame (a selector is a particular view on an atom and through hooks you subscribe to its updates).
Recoil is quite smart in its implementation, and nicely enough, you can use the reducer
pattern inside of it: we can keep all the state inside an appAtom
and useRecoilSetState
to update it.
Here appAtom
in our main db and the other selectors (reposView
, repoItemView
etc) provide the materialised views. Recoil does the rest, triggering an update when needed. If you click a RepoItem
and set its nested state to isOpen:true
, only that element will rerender (if you enable this flag).
Again, I'm over-engineering our example to illustrate how these concepts work, but creating a new selector for each component seems to be quite a common pattern. And I love how business logic (imagine a "is repo actively maintained?" concept) sits in the selector and the UI just consumes it.
🎢 (4) Side effects as data
Last point, I promise.
We seem to have it all now: centralised state, consistent hot reload, smart updating views. What more, I hear you say?
In all the examples above, when it's time to perform a side effect we just go ahead and do it. On the click of a button, we go ahead and hit the network, or persist to local storage, or manipulate the url. Seems easy enough.
Except that performing the side effects straight away makes it harder to track down what's happening at any given point in your application. Remember the official Architecture Diagram in the re-frame README?
A side effect can trigger more side effects and/or state changes. It's a first class citizen of our application. And I think we should create an Effect
data structure to represent it, similarly to how we create a reducer Action
to transition the state.
This is a concept strongly advocated by The Elm Architecture and the Command pattern, and many libraries, including re-frame, have it. There are a few ways to implement this "effects queue" but the simplest is to keep it in the app state, along the "memory db" that holds the data to be visualised by the components. So now we have two top level keys: db
and effects
.
Recoil, again, makes it pretty clean because your app can just subscribe to the dbView
which holds the ui data as we now it, and a different part of your app can subscribe to the effectsView
and perform the side effects.
Notice three things:
- We don't need to call useEffects everywhere in our application: there is just one at the root, and all effects are performed there.
- When we execute a branch in the reducer, we no longer just specify how the app state changes, we also declare which side effects will triggered. This is an amazing way to understand the behaviour of your application: "When I click this button I display a loading indicator and start to fetch the data". This is how we humans talk about these concepts, and they're nicely reflected in our code.
- Side effects execution is now explicit. You can inspect it, monitor it, pause it, replay it, persist it, rewind it (play Daft Punk song here) because it's just data™. For example, when a button is clicked you can have a look at the effects queue and decide to remove any pending http side effects if the user is navigating to another screen.
If you model side effects explicitly you can have fantastic insight on your app behaviour: check the re-frame-10x demo or the Elm Time Travel Debugger.
I know some of you might think: ugh, is it worth the effort? And the answer is yes, believe me. And if you don't believe me, believe success of the tool you're already using: isn't React just a way to model DOM side effects through explicit data instead of executing them directly?
🕵 Conclusion: where is the library
You might be thinking: ok, interesting ideas, but you talked about all of this because you have a library to pitch, don't you? You don't expect me to write this myself, do you, so where's the download link and the Quick Start page?
Sorry, there's no library. If there was it would be called The Kappa Architecture for React
, or simply reframejs
, but there's no need for it. And this is not even a pitch to say: all these things you can have if you switch to ClojureScript. Because it's not a particular framework or library: it's a set of related concepts. Sure, they feel more at home in a cljs project but they work nicely in plain React too, and I used them to create this simple multiplayer buzzer app if you want to see them in action.
Top comments (1)
Switching to ClojureScript sound much easier, if you can convince your colleagues.
What are the main disadvantages for use ClojureScript though?