6 things Reacters do that Re-framers avoid

I often hear JavaScript programmers complain about the complexity of React. "It's just another framework," they say. "It won't solve all of your problems." However, from the ClojureScript perspective, React solves the biggest problem, and the other problems are just normal programming problems. I wondered what everyone was complaining about.

I've poked my head out of the ClojureScript world and looked at what people were doing with React in JavaScript. I could see why they were having trouble.

In this article, I wanted to gather some of the React practices I've seen in the wild, show why they are problematic, and explain how we do it with Re-frame.

Table of Contents

1. Reacters load data on mount

The React Lifecycle methods give you hooks into the complete life of a component, from initialization, through rendering, and finally unmounting from the DOM when it's no longer displayed. Of course a component is almost useless without the data it depends on. And if that data exists on a server somewhere, you're going to need to fetch it.

Reacters recommend starting the Ajax request to fetch the data in the componentDidMount method. That's the first method where you can start setting the component state and have it trigger a re-render. The callback on the Ajax request should call setState with the data it gets from the server. Until that request is completed, you can indicate that it's loading, then when the re-render is triggered, you show the data.

Here's why it's a problem:

As a functional programmer, this technique screams out side-effects. I want my Views to be pure. They need to take some data and output DOM. The pattern of firing an Ajax request when the component mounts means I can't feed it my own data (if it only knows how to get it from the server), or I have to have a way of indicating that I have the data. On top of that, it quickly becomes difficult to control how many Ajax requests are firing. I know that there are probably solutions to all of these critiques (maybe caching?), but already the complexity required to write this component is making me dizzy. That complexity should exist outside of the component. It belongs in the C(ontroller) not the V(view).

Here's the problem it's trying to solve:

This technique of loading data in the componentDidMount is trying to solve a real problem, which is that we want to tie the component to the source of its data. You're saying "Whenever we show this component, we need data from here which we fetch if we don't already have it."

Here's how Re-framers do it:

In Re-frame, we keep the components, as much as possible, about rendering HTML. We don't want to do complex logic inside the component. And we certainly don't want to fetch data. Instead, they are just about converting data into HTML (using Hiccup).

So how do we solve that same problem, which is to tie a component with its data source? Well, one way to do it is to tie the display of a component to the fetching of data within an Event.

Let's say we have a component called app that chooses which screen to display based on a value from the Database.

(defn app []
  (let [current-screen @(re-frame/subscribe [:current-screen])]
    (case current-screen
      :home-screen [home-screen]
      :user-account [user-account]
      ...)))

We can create an event called :display-user-account which sets the current screen and fires off an Ajax to fetch the required data:

(re-frame/reg-event-fx
  :display-user-account
  (fn [cofx _]
    {:db (assoc (:db cofx) :current-screen :user-account)
     :http-xhrio {:url "/user-account"
                  :method :get
                  :success [:save-user-account]}}))

This Re-frame Event captures the intent that the user wants to see their account information and lists the Effects that should ensue. We solve the same problem (tying data loading and component mounting) without losing the purity of our components. Grouping of Effects should be done by an Event.

2. Reacters register a bunch of event handlers in their components

Let's face it: sometimes you need to register a window resize callback and change the sizes of things with JavaScript. I have seen components register their callback on window events in the componentDidMount, only to unregister them on componentWillUnmount. Those are the recommended places for doing those things.

Here's why it's a problem:

Like fetching data from the server, setting up callbacks is an effect. Those callbacks need to be managed. By putting the registration of callbacks inside the component, you're also cutting off your ability to guess how this component will work. Imagine making a giant list view where each element was one of those components. Are they each going to have a callback?

Here's the problem it's trying to solve:

There are components that legitimately need to change something based on a DOM event. Components need to resize or disappear when the window resizes. Or you want to have the component change color based on the window scroll event.

Here's how Re-framers do it:

Remember, in Re-frame, components are all about rendering HTML. They shouldn't have to manage anything. When you're dealing with actions from the user, like resizing or scrolling, you can capture that with an Event like :resize-window or :scroll-window. So you would register a callback that would dispatch a Re-frame :scroll-window event in the window scroll event. Note that this doesn't apply to events on individual DOM nodes. You might want to capture those using local state.

Once you have the Event being dispatched, there are several ways to implement the Event handler for it. Your components will need to react to this event's changing data. For instance, the changing value is the document.body.scrollTop. You can put that in the database and components can subscribe to it. They will be re-rendered whenever that changes.

With this solution, you've only got one event handler that anything can react to. Being part of the database, you can also use it inside of any Subscription's calculations.

That said, you don't have to do it using Events and the Database at all. You can simply create a Reagent Atom that always has the current value of document.body.scrollTop. Any component could deref that to get the current value and they'd re-render whenever it changes.

3. Reacters optimize when to render

One of the Lifecycle Methods is shouldComponentUpdate. You can override this to help React know when a component doesn't need to re-render, which could save a lot of processing. Reacters do this a lot because they start to see slowdowns in their apps.

Here's why it's a problem:

I've got enough to worry about making my app work correctly. I don't think I could do as good a job at that if I also had to make it fast.

Here's the problem it's trying to solve:

JavaScript arrays and objects are mutable. If you're mutating an object in a way that doesn't need to be re-rendered, there's a lot of potential savings if you can figure that out.

Here's how Re-framers do it:

Re-frame capitalizes on the underlying power of immutable data structures. It's easy to know when immutable data changes. The answer is: never. If you've got an old value and a new value and the references are equal, you don't have to re-render. That's baked into Reagent. And that check is super fast. If they are different, you could check if they're equal, meaning they have the same value. Or you could just re-render and let React do its job of optimizing away non-differences. Reagent does one or the other, but honestly, it's so fast I've never bothered to check.

That takes care of what a Reacter would call prop changes. There are still changes that could cause a re-render due to Subscriptions changing. The key there is that Subscriptions can be "chained" so that one Subscription is calculated from the values of others. If one Subscription in the chain calculates the same value it had previously, it won't re-calculate anything down the line, and it won't trigger a re-render. That saves another bunch of work. Subscriptions align de-duplication with optimization.

4. Reacters load data when stuff changes

Another thing we see Reacters do is fetching new data when the props change. They override componentWillReceiveProps, check whether the thing they need has changed (such as the User ID in the props), and fetch the new thing from the server. The callback will save the result in the state, causing a re-render. This is very similar to the initial fetch in componentDidMount except it's for changes, not the initial load.

Here's why it's a problem:

Of course, this has the same problems as the initial load. It's mixing an effect with our pure component. It becomes difficult to re-use the component since wherever we use it, it could start doing Ajax requests.

Here's the problem it's trying to solve:

React is pretty helpful at making the smallest changes possible to a component instead of reinitializing the whole thing. For instance, if I have a component like this:

<UserView user_id={{this.state.user_id}} />

And the this.state.user_id changes, React will simply send the new props in instead of making a whole new component. That's great, but now that the User ID has changed, that component will need the rest of the data for that User.

Here's how Re-framers do it:

Re-framers do take advantage of this great property of React except that they don't load data in the component. They would likely use a Subscription and pass along the User ID as a parameter to it, like this:

(defn user-view [user-id]
  (let [user @(re-frame/subscribe [:user user-id])]
    ...

That subscription would change value whenever the argument (user-id) changed, just like props changing in React. That would trigger a re-render. The problem still remains of fetching that new User from the server. I would expect that whatever event triggered the user-id to change would also fire off an Ajax to the server.

5. Reacters worry about when to set state

If you look through the lifecycle methods, you'll see that some of the methods talk about whether you can call setState within them and whether the component will re-render due to such a call.

Here's why it's a problem:

There are nine lifecycle methods defined in React. That's a lot to keep track of. You have to know not only the purpose of the method, but what you're allowed to do in each one. Frontend programming is already really hard. I think React simplifies it tremendously, but I wouldn't want to have to think about whether it was safe to call a method at different times.

Here's the problem it's trying to solve:

Like I said before, React simplifies things a lot. DOM programming is really complicated, if you broke it down into its steps in a way that handled all situations. The lifecycle methods each have their place, and just before a re-render, it's not really appropriate to change the state. It's running full steam ahead trying to render what it already has.

Here's how Re-framers do it:

The solution is easy: we just never call setState. We have other ways of storing component-local data, using Reagent Atoms. And we have other ways of storing global data, in the Re-frame database. Everything is reactive, meaning that components re-render when data they are looking at changes. We simply don't think about when we're allowed to change stuff.

6. Reacters use a lot of Redux boilerplate

A lot of the functionality of Re-frame also exists in Redux. Redux is a way to build a system of action handlers which result in a reactive re-rendering of the React DOM. There is a data store and components are connected into it to make them re-render. I really appreciate Redux because it's bringing functional programming ideas to the JavaScript world.

Having used Redux in a project of some size, I know some of its pain points. While it did help us clean up our code, it also had a lot of boilerplate to set up. Each component needed to be individually connected to Redux. We had to define, for each component, how the data store was transformed into props. Finally, we had to write our action handlers in a very specific way and wire them up with arcane function calls. It was a lot of work, easy to get wrong, and hard for beginners to understand. The documentation was clear, but it was a lot to learn.

Here's why it's a problem:

Like I said before, I really appreciate Redux for bringing functional programming ideas to the mainstream. However, there were some serious problems with it. Redux recommends building action handlers in a certain way. That is, they check the action tag and if they know how to handle it, they return the modified state, otherwise they return the unmodified state. The current recommendation is to use a big case statement with each action type defined. There are ways to break this apart based on different sections of your data store (keys in the object), but you still need big case statements, which I believe are hard to read and they need to be centralized.

Further, Redux recommends separating your components into two types: Presentational Components and Container Components. The presentational ones are like the ones you're familiar with, but the container components are connected to the Redux store and are subscribed to updates. To connect those components, we define two functions called mapStateToProps and mapDispatchToProps which transform the Redux state into props and callbacks for the connected component. There's just a ton of work to do just to get things connected. Frankly, I think it's just too much, for beginners and experts alike.

Here's the problem it's trying to solve:

Applications have complex data flows. We want to centralize the storage of data, yet allow arbitrary access to that data. Centralized storage means we can all share access. But the way it's organized centrally is not necessarily the way our UI components are organized. We need a good way to manage the data and a good way to get data out, all while staying reactive so that React can re-render appropriately.

Here's how Re-framers do it:

Re-frame is based on the same principles as Redux. However, Re-frame takes more of a framework approach, while Redux is more like a couple of libraries. For instance, Redux's documentation says your application should have only one store. However, what Redux gives you is a function to create a store. Re-frame, on the other hand, also says you should have one Database (the equivalent of a Redux store), but it creates it for you and you never have to think about it.

When you define Event Handlers (the equivalent of the parts of reducer functions in Redux), you don't have to write a big case statement. Re-frame handles the dispatch for you. You just have to define what each Event type does. You don't have to do it centrally. Each registered Event Handler could be in its own file, or you can put them all in the same file. It's up to you.

Finally, there's no need to explicitly "connect" components to the Database. Reagent will automatically detect which components use which Subscriptions and have them re-rendered when the Subscriptions change.

The freedom from boilerplate makes Re-frame refreshing and quite pleasant to use.

Conclusion

React is great. But it's almost not a framework. It's very low level. It's great for building on top of. Specifically, it doesn't have any means of capturing user intent, no way to store application data, and no way to manage side effects. Because people think of it as a framework, Reacters have been trying to figure out how to solve all of those necessary features from within the confines of React itself, which gives you almost nothing more than the Component class.

Redux tries to give you a way to do all of those necessary features. But it's just got too much error-prone boilerplate. In short, it gives you a way to do all of those things, but that way is like threading a needle. You'll have to lick the end of that thread a lot before it will go in right.

I believe Re-frame, more than simply solving these problems, has made most of the problems non-issues for the programmer. That's the kind of thing I expect from a framework. As the joke goes "We had a state problem, so we added Redux. Now we have two problems." The Re-frame joke is "We had 100 SPA problems, so we chose Re-frame. Now we have five problems."

More posts in this Re-frame Series