by Vitaliy Akimov
When to use React Suspense vs React Hooks
React Suspense is to a Monad as Hooks are to Applicative Notation
Monads and Applicative Functors are extensively used in functional programming. There is a relationship between them and React Suspense for Data Fetching and React Hooks APIs. This is a quick and simple introduction to Monads and Applicatives along with a description of their similarities.
The post is about future React Suspense for Data Fetching, not about recently released React Suspense for Code Splitting (
Pure functions make the code modular, predictable and easier to verify. But they also significantly increase verbosity. Here is a statement from Phil Walder’s Monads for functional programming(1995) tutorial:
It is with regard to modularity that explicit data flow becomes both a blessing and a curse. On the one hand, it is the ultimate in modularity. All data in and all data out are rendered manifest and accessible, providing a maximum of flexibility. On the other hand, it is the nadir of modularity. The essence of an algorithm can become buried under the plumbing required to carry data from its point of creation to its point of use.
Monads solve this problem for Haskell. And Suspense/Hooks solve the same problem in React.
So what is a Monad? It is a simple abstract interface which has two functions, let’s call them
of— takes any value and returns some monadic (effectful) value
chain— takes an effectful value and a function from any value to an effectful one and returns another effectful value
The effectful values there may encapsulate any concrete implementation-specific information. There are no requirements what exactly it should be, it is some opaque data. The interface’s concrete implementations should follow a set of laws, and this is it.
There is nothing to say more about monads since they are abstract. They don’t necessarily store anything, wrap or unwrap anything or even chain anything.
But why do we need this if it is so abstract and defines almost nothing? The interface provides an abstract mean to compose computations with side effects.
In computer science, Monads first appeared for studying side effects in imperative languages. They are a tool to embed imperative worlds into a pure math world for further studying.
This way if you want to convert your imperative program into math formulas representing it, doing this with Monad expressions would be the simplest and the most straightforward way. It is so straightforward what you don’t even need to do it manually, there are tools that do it for you.
Haskell has a syntax sugar called do-notation exactly for this. This makes writing imperative programs in Haskell possible. There is a special tool in its compiler. It converts such imperative programs into a Monadic pure Haskell expressions. The expressions are close to math you see in textbooks.
There are such extensions, namely generators, async and async generator functions. JavaScipt JIT compiler converts async and generator functions into concrete built-in API calls. Haskell doesn’t need such extensions. Its compiler converts do-notation into abstract Monads interface function calls.
Here is an example of how async functions simplify sources. This shows again why we need to bother having a syntax for effects.
We can convert some effects into others. This way we can write async code using Generators.
This conversion trick can be applied to other effects too. And apparently, just Mutation and Exception are enough to get any other effect. This means we can turn any plain function into an abstract do-notation already. And this is exactly what Suspense does.
When the code encounters some effectful operation and requires suspension it throws an exception. It contains some details (for example a Promise object). One of its callers catches the exception, waits while the promise in the argument is settled, stores the resulting value in a cache, and re-runs the effectful function from the beginning.
After the Promise is resolved the engine calls the function again. The execution goes from its start, and when it encounters the same operations it returns its value from the cache. It doesn’t throw an exception and continues execution until the next suspension request or the function’s exit. If the function doesn’t have any other side effects its execution should go the same paths and all pure expressions are recalculated producing the same values.
Let’s re-implement Suspense. Unlike React, this one works with the abstract Monads interface. For simplicity, my implementation also hides a resource cache. Instead, the runner function counts invoked effects and uses the current counter value as a key for the internal cache. Here is the runner for the abstract interface:
Now let’s add a concrete Async effects implementation. Promises, unfortunately, aren’t exactly monads since one Monad law doesn’t hold for them, and it is a source of subtle problems, but they are still fine for our do-notation to work.
Here is concrete Async effect implementation:
And here’s a simple example, it waits for delayed values before rendering proceeds:
The sandbox also contains
Component wrapper. It turns an effectful functional component into a React component. It simply adds
chain callback and updates the state accordingly. This version doesn’t have a fallback on threshold feature yet, but the last example here does have it.
The runner is abstract, so we can apply it for something else. Let’s try this for the
useState hook. It is a Continuation monad, not a State monad as its name may suggest.
Effectful value here is a function which takes a callback as an argument. This callback is called when the runner has some value to pass further. For example when the callback returned from
useState is called.
Here, for simplicity, I use single callback continuations. Promises have one more continuation for failure propagation.
And here is a working usage example, with most of “kit.js” copy-pasted, except the monad’s definition.
Unfortunately, this is not exactly the
useState hook from React yet, and the next section shows why.
There is another extension for do-notation in Haskell. It targets not only Monad abstract interface calls but also calls of Applicative Functors abstract interface.
Applicative interfaces shares the
of function with Monads and there is another function, let’s call it
join. It takes an array of effectful values and returns a single effectful value resolving to an array. The resulting array contains all the values to which each element of the argument array was resolved.
If there is a default implementation, why do we need Applicative Functors? There are two reasons. The first one is not all Applicative Functors are Monads, so there is no
chain method from which we can generate
join. Another reason is, even if there is
join implementation can do the same thing in a different way, probably more efficiently. For example, fetching resources in parallel rather than sequentially.
There is an instance of this interface for Promises in the standard runtime. It is
Promise.all(ignoring some details here for simplicity again).
Let’s now return to the state example. What if we add another counter in the component?
The second counter now resets its value when the first one is incremented. It is not how Hooks are supposed to work. Both counters should keep their values and work in parallel.
This happens because each continuation invocation erases everything after it in the code. When the first counter changes its value the whole next continuation is re-started from the beginning. And there, the second counter value is 0 again.
In the run function implementation, the invalidation happens at line 26 —
trace.length = pos — this removes all the memorized values after the current one (at
pos). Instead, we could try to diff/patch the trace instead. It would be an instance of Adaptive Monad used for incremental computations. MobX and similar libraries are very similar to this.
If we invoke effectful operations only from a function’s top level, there are no branches or loops. Everything will be merged well overwriting the values on the corresponding positions, and this is exactly what Hooks do. Try to remove the line in the code sandbox for two counters above.
Using Hooks already makes programs more succinct, reusable and readable. Imagine what you could do if there were no limitations (Rules of Hooks). The limitations are due to runtime-only embedding. We can remove these limitations by means of a transpiler.
Effectful.JS is a transpiler for embedding effectful into JavaScipt. It supports both Monadic and Applicative targets. It greatly simplifies programs in the designing, implementing, testing, and maintaining stages.
Effectful.JS is not exactly a transpiler but rather a tool to create transpilers. There are also a few predefined ones and a lot of options for tuning. It supports double-level syntax, with special markers for effectful values (like
awaitexpressions in async functions, or Haskell’s do). And it also supports a single level syntax where this information is implicit (like Suspense, Hooks or languages with Algebraic Effects).
I’ve quickly built a Hooks-like transpiler for demo-purposes — @effectful/react-do. Calling a function with names starting with “use” is considered effectful. Functions are transpiled only if their name starts with “use” or they have “component” or “effectful” block directive (a string at the beginning of the function).
There are also “par” and “seq” block-level directives to switch between applicative and monadic targets. With “par” mode enabled the compiler analyzes variable dependencies and injects
join instead of
chain if possible.
Here is the example with two counters, but now adapted with the transpiler:
For demo purposes, it also implements Suspense for Code Splitting. The whole function is six lines long. Check it out in the runtime implementation @effectful/react-do/main.js. In the next example, I’ve added another counter which rendering is artificially delayed for demo purposes.
Algebraic Effects are often mentioned along with Suspense and Hooks. These may be internals details or a modeling tool, but React doesn’t ship Algebraic Effects to its userland anyway.
With access to Algebraic Effects, users could override operations behavior by using own Effect Handler. This works like exceptions with an ability to resume a computation after
throw. Say, some library function throws an exception if some file doesn’t exist. Any caller function can override how it can handle it, either ignore or exit process, etc.
EffectfulJS doesn’t have built-in Algebraic Effects. But their implementation is a tiny runtime library on top of continuations or free monads.
Invoking a continuation also erases everything after the corresponding
throw. There is also special syntax and typing rules to get Applicative (and Arrows) API — Algebraic Effects and Effect Handlers for Idioms and Arrows. Unline Applicative-do this prohibits using any anything which requires Monad operations.
The transpiler is a burden, and it has its own usage cost. Like for any other tool, use it only if this cost is smaller than the value you get.
As an example, Effectful.JS can replace Suspense, Hooks, Context, and Components State with tiny functions. Error Boundaries are the usual
try-catch statements. Async rendering is an async scheduler. But we can use it for any computations, not only for rendering.
There are a lot of other awesome application-specific uses, and I’m going to write more about them soon. Stay tuned!