Where to Store State in Re-frame?

Re-frame gives us different ways to manage application state. Each of those ways has different properties, and by putting them together in the right way, we can give our app's state the properties we want.

This guide goes through the different ways of storing state and how to choose between them.

Table of Contents

More posts in this Re-frame Series

Introduction

There are four tools to store state in a Re-frame application.

  1. Server or Third-Party API
  2. Re-frame Database
  3. Component-local
  4. Reagent Atoms

All of your frontend's state should be in one of those four places. But which one should you choose for any particular bit of state really depends on what you want to do with it. Luckily, they're each so constrained that the decision is clear. Yet another way that the framework helps you build apps.

The constraints of your state

State is intimately bound up with space and time. Where is the state stored, read, and written from? How fast does the state change? And how long will you need to store it? All of these questions are important considerations that determine the best tool for the job.

Locality (Where is it read and written?)

Which components will need to read or write to this state? This is perhaps the most important and overriding property. It dictates so much of the decision, so let's talk about it first.

Let's ask this question another way: when this state changes, what needs to know? Is it just one component? Multiple components? Multiple browser tabs? Multiple users?

But just as important, what will be changing the state? Is it the same component that is reading it? Or can multiple components change it? Or does it change from some other activity, such as a browser resize event? Or maybe the server sends you a message that causes your state to change.

If the reading and writing is confined to just one component, consider keeping the state in component-local state. You can always move it out later.

The Single-Page App will be loaded in a single browser tab. So typically anything requiring multiple browser tabs or users needs to go out to the server, which can then send out the changes to the individual frontends that need it. Unless you're doing some kind of in-the-browser Peer-to-peer stuff (like Web RTC), the client-server model dictates that the state needs to be on the server.

But if your state is accessed by multiple components in one single tab, you have another choice. You could store it in the Re-frame Database, which is probably where it should go. But you could also create your own Reagent Atom to store it in as well. Like I said, the Database is probably the right place for it, but if your state changes fast enough, you may want to think about putting it in its own Reagent Atom.

Bottom line: keep state as local as possible. If only one component is reading and writing it, make it component-local. If only one tab needs to see it, keep it in the Database (or a Reagent Atom if it's updated frequently). If multiple tabs need to see it, it's got to go to the server or third-party API.

Update frequency (How fast are writes?)

State that is changed by typical UI interactions does not get updated that much. If you have a change to your state based on some button clicks, it's a low-frequency change. How many clicks per second could you even expect?

But some changes are really fast. If I want to store the window width and height in response to a resize event, wow, that thing changes really fast. If I have to dispatch an event, which goes into a queue, which assoces a new value into a deeply nested map, which then updates the Database, which triggers subscriptions to recalculate, wow, that's a lot to do. The resize event fires a lot while the window's size is being changed. It's much better to avoid the machinery of Re-frame and just store that in a Reagent Atom. Components can still re-render when it changes, and you can still use them in Re-frame Subscriptions. But Atoms let you avoid a lot of work for such highly volatile state.

Bottom line: if your state changes very frequently, consider keeping it out of the Re-frame Database.

Transience (How long do you need to keep it?)

How long will you need to keep your state? At the smallest timescales, you only need to keep state for a very short time. For instance, as you type into a text box, the content is constantly changing and you probably don't need to keep the individual states for very long. You only care about the final contents of the text box. This is a good use for component-local state. When the component goes away, the state is gone.

On the other end of the timescale, the user makes a very important action and you want to save that forever, or as long as the user has an account. This kind of stuff needs to be stored on the server, not in a browser tab which could be closed at any moment.

In between, we have state stored in the Re-frame database or in global Reagent Atoms. It's stored as long as the browser tab is open on that page, but it won't be available if the browser is reloaded.

Bottom line: store state as transiently as possible. If you need it forever, definitely on the server. Just for the time between keystrokes? Definitely in the component. Somewhere in-between? Somewhere in the global scope of your application, either in the Database or in a global Reagent Atom.

Caching data from the server

When we create a Re-frame application, we want our UI to update in response to state changes. That typically and most idiomatically means that our components deref Subscriptions or Reagent Atoms. When they do that, they'll automatically be re-rendered when the state changes. However, how do we react to data stored on the server?

The answer is usually to cache the state store on the server either in the Database or a Reagent Atom. When the state on the server changes, you update the cache, and everything re-renders.

Now, we programmers know that caching is one of the two big problems in Computer Science. We have a big decision to make---when do I update the cache in response to a user interaction?

Here's an example. Let's say I have a Delete button that deletes an item from my shopping cart. The shopping cart needs to be shared among multiple tabs, so the state has to be stored on the server. But I also have a cache of it in the Re-frame database. What happens when I click that button? Do I remove it immediately from the cache when I send the Delete event to the server? Or do I send the Delete event and wait for the server to respond before I update the cache?

Optimistic update

If I delete the item immediately in the c ache, that's called optimistic update. I'm assuming that the server will successfully handle the delete, which happens 90% of the time anyway. It lets me show the user the result of their action right away instead of waiting. The problem is you then have to handle the 10% of the time that it didn't happen (like if the request times out). Obviously, this can be problematic for things that are very important to get exactly right. For instance, you don't want to tell the user their credit card was charged if it really wasn't.

Pessimistic update

You could send the event to the server, then wait for the response. That could take a while. You'll want to show the user something to indicate that the button click actually was registered. Typically you use a loading spinner and disable the button so they don't fire two events.

Then when the request is finished, you know if it was successful. If it succeeded, you can modify the cache to reflect it and everything is fine. If it failed, you might notify the user, but your state is never incorrect---it's only ever a little out of date.

However, there still is one case that you need to handle, which is when the request times out. You don't know if the event actually went through and the response didn't make it back. It's rare but it can happen. Distributed systems are hard. You can make retries safe by making the operation idempotent. But in general, this is just a hard problem.

Tools for storing state

Now that we know all of the things to consider for how to choose which tool to use, let's look at the mechanics of using each tool.

Server or Third-Party API

When to use

Just to summarize, the constraints we've seen mean the stuff we store on the server is either:

  1. State that needs to outlast the browser tab.
  2. State that needs to be accessed from multiple browser tabs.

Reading and writing

Accessing the Server or a Third-Party API, both reading and writing, requires a Re-frame Effect. They're changing (or getting information from) the outside world. Communication with the server will probably be through Ajax calls or something like WebSockets.

Consider using the HTTP Effects Handler for Re-frame. But you can write your own Effects as well.

Component-local state

When to use

We want to prefer component-local state for everything because it has such limited scope. Use it if:

  1. Only one component needs this state and you can lose the state when the component is unmounted.

If a couple of related and adjacent components need the same state, consider combining them into one component so you can share component-local state. I do this all the time with input forms.

Reading and writing

Component-local state is created by making a Reagent Atom inside of a Form-2 or Form-3 component. Reading is easy---just deref the Atom. Writing is done with swap! and reset!. Since everything is local to the component, there's little benefit to naming and otherwise abstracting the writes.

Re-frame Database

When to use

There are essentially two uses for the Database. The first is the app state that tracks what the user is doing. The second is as a cache for stuff stored on the server. Use the Database for:

  1. State used by multiple components for a single browser tab.
  2. State that is cached from the server or other third-party APIs.

Reading and writing

You write to the Re-frame Database by returning a :db Effect from an Event (or by using the DB Event shortcut). You read from the Database in an Event with the :db Co-effect that is implicitly added to all Event handlers.

Because you're using Events, you are explicitly tying signals you get from outside of your application (user input, messages from the server, etc) to modifications to the Database.

You can also read from the Database using Subscriptions.

Reagent Atoms

You can declare global Vars containing Reagent Atoms. Reading from them is easy. You can then deref them directly in Components or use them in Reactive Subscriptions. Or if you need to read from them in Events, you could create a Co-effect to get the current value.

Because you should be using Reagent Atoms for highly volatile state, I suggest avoiding the Re-frame machinery (Events, Effects, etc) for writing. Instead, write directly using swap! and reset!, but only in one place.

Here's an example that stores the current window size, which changes quite frequently while the user drags the window resize handles.

(defonce window-size
  (let [a (r/atom {:width  (.-innerWidth  js/window)
                   :height (.-innerHeight js/window)})]
    (.addEventListener js/window “resize”
      (fn [] (reset! a {:width  (.-innerWidth  js/window)
                        :height (.-innerHeight js/window)})))
    a))

Now you can access the width and height in Subscriptions and Components by derefing window-size.

Browser LocalStorage

I want to mention another possible place to store state. There's a browser API called LocalStorage that will let you keep some state in the browser between tabs. It's not kept with the user's account. It's kept with the browser, accessible from all tabs that are on the same domain. You can't access it from another browser (even owned by the same person logged into the same account). Those constraints mean that LocalStorage is great for caching and not much else.

Caching is important, so LocalStorage should be a consideration, but it should be layered on top of the other state mechanisms. One use for it is to remember state from the server so that you can display it quickly on page load while fresher data is being fetched. So you're caching the server's state.

Another use is to cache messages before they get sent to the server. If you're going to send a message to the server, but your network connection is dropped, the message is going to fail. Where do you keep it while you retry? You might have to keep it for a long time while you wait for the network to come back up. LocalStorage can help with that. This guide from Mozilla has some good recommendations.

More posts in this Re-frame Series