React Suspense and ClojureScript

React recently has been quietly releasing a lot of updates that will, in my opinion, be transformative in how we build web applications. And they do it by releasing small composable elements that can be used a-la carte (or not at all) to do very very cool stuff.

One of those elements is the “Suspense” element. This is something like an exception handler but for data fetching. When a child element of a Suspense elements “suspends” during render, for example if it needs to load data or code, then React can detect that and show a fallback element (like a loading spinner or such), then try again when the required resource is available.

In the newly-released experimental “Concurrent Mode”, the Suspense element can do more things - it can be synchronised with other suspense elements so that they show in a specific order, it can decide to wait a little bit to avoid “flashing spinners” and more. This is all powering the new Facebook page and dogfooded there, so there’s a lot of real-life use cases they’re trying to solve.

The way this works behind the scenes, is that a component can throw a Promise during render. React will then “suspend” it, and when the promise resolves, it will re-render it.

It turns out that IDeref is a nice little abstraction that can be used for this - the consuming component just derefs a property passed in, without caring what happens behind the scenes.

Here’s some toy example code using hx:

;; data is a deref-able, but the component doesn't know
;; what's going on behind the scenes
(defnc SubMain [{:keys [data]}]
  [:div "Sub main " @data])

(defnc Loading []
  [:div "Loading..."])

(defnc Main []
  ;; you obviously shouldn't load-data when rendering, but this is a toy example
  (let [data (load-data #(fake-fetch "PAYLOAD"))]
    [:div "Hello"
    ;; need to wrap Loading with hx/f to pass it in as a prop
     [React/Suspense {:fallback (hx/f [Loading])}
      [SubMain {:data data}]]]))

And here’s how the magic happens:

(defn timeout-promise [millis]
  (js/Promise.
   (fn [resolve reject]
     (js/setTimeout resolve millis))))

(defn fake-fetch [payload]
  (-> (timeout-promise 5000)
      (.then (fn [] payload))))

(defn load-data [fetch-fn]
  (let [p     (fetch-fn)
        state (atom {:loaded  false
                     :payload nil
                     :error   nil
                     :promise p})]
    (.then p
           (fn [payload]
             (swap! state assoc
                    :loaded true
                    :payload payload))
           (fn [error]
             (swap! state assoc :error error)))
    (reify IDeref
      (-deref [_]
        (cond
          ;; if promise has been resolved, return the payload, render as usual
          (:loaded @state) (:payload @state)
          ;; if promise has been rejected, throw the error
          (:error @state) (throw (:error @state))
          ;; else, promise has not been resolved, so suspend by throwing the promise
          :else (throw (:promise @state)))))))

You could test this right now with the latest stable React release and it will work. To get the error handling to work you need a custom ErrorBoundary, which React doesn’t provide out of the box, but that’s a topic for another time.

There’s a lot of potential to this – you could implement an ISeqable that fetches data from the network, an ICounted that can do pagination etc etc.

If you’re interested in learning more about what React is cooking, there’s a blog post and also some enlightening videos from the recent React Conf.

11 Likes

Nice recap! like the idea of using IDeref

I think it’s worth pointing out that despite the warning about not fetching data during render, that’s precisely the design that Suspense encourages. Each component on first load initiates a fetch, might throw a Promise which tells the wrapping Suspense component to render the fallback loading state if defined. Suspense implementations expect that once the request has been fulfilled, it’s stored in cache so that subsequent renders don’t re-trigger the whole flow.

I think the value here is in allowing us to define more natural UIs where a component fetches the data it needs and once it arrives it can pass it to children which themselves might fetch more. Imagine rendering a Datomic Entity value, pulling a card many attribute, and passing those entities down the UI tree. Subcomponents can better manage the keys they need specifically (ala Spec 2, Rich’s “Maybe Not” talk).

Many critiques I’ve seen of suspense suggest you should just “fetch all the data in advance” and pass it to your UI as if that’s an easy, solved problem. In practice I find I do a lot of work marshaling the various data requirements of my components from the top level.

What happens if a state change occurs which should invalidate the cached fetch? That part I’m still pretty unclear about

Anyone else trying out the Suspense features?

1 Like

It’s a little bit more nuanced than that – fetching data during render will invariable lead to “waterfalls”, which to my understanding means that component X will render, fetch some data with useCallback or componentDidMount, which will lead to Suspense, which will then resume, then its children will then render and also fetch some data and so on. Which means the user sees a waterfall cascade of spinners or “Loading…” elements.

The React people advise you to instead try to fetch all the data from outside the render tree (e.g. during a router action), and then use strategically placed Suspense elements to control where exactly the “Loading…” elements appear.

Yep! This aligns with my understanding as well. Saw a trending tweet that likened Suspense to Error Boundaries for data fetching which I think sums it up nicely

If all the data is fetched before render, outside the render tree (like next.js) there would be no need for progressive loading when render occurs right?

It’s only when a fetch invoked during initial render, which throws a promise to trigger the suspense, that the loading staging is valuable. This seems to me to encourage the “waterfall” design you outlined – I can’t tell if this is good or bad though :slight_smile:

I can say that I see a lot of sites in the wild that could benefit immensely by consolidating the loading experience which I hope suspense helps

I think that Suspense is a part of a two pronged approach to helping with delivering better UX for data loading.

To reiterate what’s been said here, Suspense for data fetching allows you to localize reads by encapsulating your loading state of your components in the same way that error handling encapsulates error states. You don’t need to coordinate the error state of each component and pass down special handlers to any component you want to handle errors for; instead, you catch whatever errors happen anywhere in the subtree and handle it. A parent component never needs to know the data dependencies of a component in order to have a good loading experience.

The second part of the problem is optimizing fetches, which is harder to do at runtime because it requires a parent concretely knowing ahead of time what the actual data requirements are for any component in a subtree. Suspense doesn’t solve this exactly, but by removing the need to coordinate loading state, it allows us to focus on this problem specifically. An event (such as navigation, or a mouse over of a link, etc.) can kick off the fetching of data to preload the data requirements for components. It becomes a matter of statically deriving and registering components data requirements and relating that to certain events which could trigger render of those components. This can be done with a compile-time system, such as a macro that registers a query with a component or page and then an event that calls out:

(defn on-navigate [page]
  (preload-data page)
  (navigate-to! page))