scala.js and react Suspense

scala.js and react Suspense

You can use scala.js with react and Suspense. Suspense is a mechanism that communicates that a component cannot complete its rendering due to unfilled depnedencies. A parent Suspense component receives the message, then renders a fallback component until it is signalled that the dependency has been fulfilled and the original component can fully render.

The communication mechanism is a thrown js.Promise. When it resolves, the Suspense parent knows that the dependency has been fulfilled and the parent draws the child.

Clearly this is not very FP oriented. When we look at suspense, we really just see that its a way to push code for conditional rendering into a parent component. The alternative approach is to use in intemediate component that fetches the data then renders the child conditionally. Using the Suspense mechaninsm removes the need for the intermediate component and pushes the asynchrony into an external cache that captures the dependency’s status and results external to the “using” component. My initial reaction is that while useful, this is not a big change. That though is echoed in this reactjs blog: https://reactjs.org/blog/2019/11/06/building-great-user-experiences-with-concurrent-mode-and-suspense.html. You should note that to use “concurrent mode” easily, FB created relay. Realy is not a library, but a framework, involving a wide array of compiler and library machinery to make concurrent mode more useful. It feels very heavy.

Let’s look at a quick example. All of the examples use https://github.com/aappddeevv/scalajs-reaction. We will code someting similar, but even smaller, like https://codesandbox.io/s/frosty-hermann-bztrp. This is the same type of example provided in links from the reactjs blog.

We’ll use js-semantics mostly, that means a twisty knot of js.Promises (vs other effects) and throwing exceptions.

  /** Create a js.Promise that returns a value in delay_ms. */
  def promiseWithDelay[T](fulfillWith: T, delay_ms: Double = 5_000) =
    new js.Promise[T]({(resolve, reject) =>
      js.timers.setTimeout(delay_ms){
        println(s"XXXX: Resolving promise w/ timeout $delay_ms, $fulfillWith")
        resolve(fulfillWith)
      }
    })

  // only usable in the browser, since its a micro-cache, its very mutable
  case class Resource[E, T <: scala.AnyRef](
    promise: js.Promise[T],
    var result: T,
    var status: String = "pending",
    var error: Option[E] = None
  ) {
    // attach to the effect
    promise
      .jsThen(v => { status = "success"; result = v })
      .jsCatch(e => {status = "error"; error = Option(e.asInstanceOf[E]) })

    def read(): Either[E, T] = { // should probably be an Option[Either[E,T]], blah
      if(status == "pending") throw new js.JavaScriptException(promise)
      else if(status == "error") error.toLeft(result)
      else Right(result)
    }
  }

  case class User(id: String, name: String)
  

This simulates the one element cache that throws an exception if you try to access it. It’s lot like accessing an Option that is really a None–a ElementNotFoundException is thrown. The one-element cache external to the component may also remind you of redux. Redux is really just a global synchronization point for asynchronous data and application state. Redux gets messy when you put too much information into the global application state though and Suspense or caches lower in the rendering tree are one way to remove the need for redux.

Our main component, for simplicity, will declare the fetch outside the actual component. This simulates a hook that initiates the fetch inside the component when it is first drawn but keeps the code in this example very simple. Note that by declaring this resource in the comonent but not in the component’s “apply” method, it starts fectching as soon as the component object is instantiated by scala. Which may be way to early and at a time you may not know the parameters for what to fetch. Generally, taake the below as the intent “call the data fetch before rendering the component.” You could start this fetch in the router or use some other application-level mechanism. If we could use dynamically imported “code” in scala.js like you can in javascript, the “fetch” would start when you load the “code.”

object MyComponent {

  // our primary resource
  val user = {
    val user = User("1", "George");
    Resource[Throwable, User](promiseWithDelay(user), user)
  }
  ....
  }

Inside the MyComponent functional component (not shown yet) we access the user resource. The only real requirement is that it throw a js.Promise when we try to access the resource. A bit crazy I know.

Let’s put togther the functional component, MyComponent, that is dependent on that resource:

object MyComponent { 
  ...
  trait Props extends js.Object {
  }
  def apply(props: Props): sfc(props)
  val sfc = SFC1[Props]{props =>
    // throws a js.Promise if the resource is not ready yet
    val u = user.read()
    div(s"User is ${u.name}")
  }
}

That’s all we need to do. Notice that there is no logic in case the user is not available.

Normally if we use a fetcher hook, like this one https://appddeevvmeanderings.blogspot.com/2019/10/scalajs-react-fetcher-hook_48.html, we would do something like:

object MyComponent {
  val sfc = SFC1[Props]{ props =>
    val (fstate, requestFetch) = useFetcher()
    fstate match { 
      case Available(user, _, _) => div(s"User is ${u.name}")
      case Error(msg) => div(s"Fetch error! $e")
      case _ => div("Waiting on data...")
    }
  } 
}

You can see that we have logic in the component to display an alternative visual if the data is still loading. React components are not pure functions so we cannot worry too much about throwing exceptions, it’s what reactjs wants

In all fairness, we would not code the component like above. You would probably do something more sensible like below because at a certain point in the tree, you know what to render when data is missing. Sometimes that knowledge is very close to the point where the data is needed to render a component, sometimes not.

object MyComponent {
  val sfc = SFC1[Props]{ props =>
    val (fstate, requestFetch) = useFetcher()
    val (user_name, error) = fstate match { 
      case Available(user, _, _) => (user.name, false)
      case Error(e) => ("no name", true) 
      case _ => ("no name", false)
    }
    Fragment(
      when(error)(div(s"There was an error".)),
      div(s"User is ${u.name}")
    )
  } 
}

To use Suspense, the parent uses the Suspense component:

object Parent {
  def apply() = sfc
  val sfc = SFC0 {
    val cprops = ???
    Suspense(fallback = div("Waiting on data"))(MyComponent(cprops))
  }
}

Suspense pushes the “waiting on dependency” display logic into the parent without the parent needing to know the child’s dependency. It’s not clear to me that removing knowledge of the data dependency in the Parent is worth the complexity of Suspense.

In fact, this is actually just bad practice. From a separation of concerns perspective, data fetching is not really a “view” concern and should be fatored out.

At the very least, data fetching should go in its own “component” and some flavor of data passed to a component that knows how to render the data or “empty” data–whatever that means to the component. An intermediate component that sets up the fetecher and handles fallback display logic is cleaner design wise. The intermediate component could be a fairly generic comonent and represents the rendevous point (cache) for the asynchrous action. This approach pulls everything out of both the parent and child.

In today’s interfaces, we might make a component display visuals based on “optional” data depenency, e.g. a controlled component receives a js.UndefOr[Data] property along with a “inProgress: Boolean” property. The child should take into account “no resuts” as well as “still getting results” in its display, e.g., show a shimmer list. In other words, the child may know how to display itself to reflect an incomplete data state. For large 3rd-party professional widget sets, they already support this concept.

Where Suspense is more useful is when the parent should decide on layout and component usage based on a complex state of dependencies and for new widgets built by the programmer. Perhaps the indicator for “fetching” is in another part of the interface that the child does not know about. Or, perhaps, you have a component that does not know how to display the concept of “waiting on dependency” properly and you need to factor that out.

So to me, at least, Suspense is a bit of head scratcher when used to provide alternative rendering logic. It feels like it is really just the codification of a pattern using a funky control mechanism that is useful in some situations and less useful in many others. It’s also unfortunate that the communication protocol to communicate a dependency’s status, throwing a js.Promise, is not FP friendly.

Also, if you are building a component, you should have a customizable way to display itself when it does not have all the data needed to display itself. And if you need more comprehensive control, just bump the fallback logic up one level–it’s really that easy. With currying, this even becomes much nicer.

I would much rather just use a component that renders another component when an effect completes, say a zio effect (or js.Promise), with an alternative fallback to display until it completes. The effect could cover both data and code (remote component fetch via dynamic import). Wait a second, we can already do that, … :-).

Comments

Post a Comment

Popular posts from this blog

zio layers and framework integration

typescript and react types

dotty+scala.js+async: interesting options