scala.js: recoil state management and graphql, wish it had a zio bifunctor

There are many state solutions for react including mobx and redux. In scala world, there is diode and a few others. In plain react, you can use hooks and context to help manage state. Hooks and context can take you pretty far, but at some point you want something more sophisticated.

In a true reactive application we want a dependency graph that updates itself, potentially asynchronously, as dependencies change. The graph should then influence specific parts of the UI to render.

In scala world, you can create a reactive graph pretty easily, however, on the browser side, there have been very few “graphy” solutions until recoil (blog) came along. Recoil was just open sourced by facebook. It has on decent API–although not scala friendly. But that’s Ok. If it can help simplify UIs, it could be a good play.

Though still in experimental status, reactjs supports Suspense and other “concurrent” features that allow a component to “signal” its dependencies are not yet available. When the signal is raised, a parent anywhere in the tree above can declare that it will handle the suspense and draw an alternate UI. For example, it is considered a best practice to draw a shimmered version of a component that cannot full render itself with data. This sets the expectation of what the UI will look like with the user. The dependency is typically data fetched from the server.

While working on a scalajs-reaction (docs) project I needed some better state management, nothing outrageously hard, but I had a page that had several parts to it that need to be synchronized together. The page dealt with “searches” and “recommendations”. There could be a searches, multiple similar searches and multiple recommendations at one time–fairly standard.

Managing this state is not hard but became tedious as each component had some state cascaded to it from the parent. However, once multiple sub-components needed the same state and so on, I turned to recoil. Having used redux and mobx, I was looking for something easier and designed in a “reactive dependency graph” way.

recoil API

In recoil, you state your dependencies as a graph. Each node has an ID and a “getter” (a recoil value) or a “getter and setter” (recoil state). Here’s a few definitions that use recoil and graphql (apollo client) to handle the dependencies. This is a simple example in the sense the state is not very complicated. Only a few elements of the recoil graph are shown.

I’ve not worked out all the type inference yet in the scala.js API, so some types are repeated in the code below a bit too often.

// the current search
val searchIdState = atom[String](AtomOptions("searchId", null))

// ids of similar searches
val similarIdsState = atom[IdArray](AtomOptions("similarIds", emptyIds))

// all possible searches
val allSearchesState = atom[SearchesArray](AtomOptions("allSearches", emptySearches))

/** Recommend list of ids. */
val recommendationSelections = atom[IdArray](AtomOptions("rselections", emptyIds))

// all searches but with the "current" search filtered out
val filteredSearchesSelector = readonlySelector[SearchesArray](
    new ReadOnlySelectorOptions[SearchesArray]{
      val key = "filteredSearches"
      def get(accessors: ReadOnlyAccessors) = {
        import accessors.value
        val searches = value[SearchesArray](allSearchesState)
        val id = value[String](searchIdState)
        searches.filter(_.id.map(_ != id) getOrElse false)
      }
    })
  
// recommendations
val recommendationsSelector = readonlySelector[RecommendationsArray](
    new ReadOnlySelectorOptions[RecommendationsArray]{      
      val key = "searchRecommendations"
      def get(accessors: ReadOnlyAccessors) = {
        import accessors.value
        val searches = value[IdArray](similarIdsState)
        val id = value[String](searchIdState)
        val client = Main.aclient
        if(searches.length == 0) emptyRecommendations
        else client.query[Q.Data, Q.Variables](QueryOptions[Q.Variables](
          query = Q.operation,
          variables = Q.Variables(searches, js.Array(id))
        ))
          .map(_.data.map(_.recommendations) getOrElse emptyRecommendations)
      }
    })

The apollo graphql client also has the ability to use hooks and the query above could go be expressed inside the component with a hook. The latest version of apollo query also has some useful caching capabilities where it can act as a cache for the entire application. You can receive notices upon change and have components rerender. In a way, its doing a bit of what other state solutions do but using graphql “schema” syntax.

In our case though, we are using recoil and for that we can embed a call through the apollo client directly in recoil.

Everything runs asynchronously and is memozied to avoid unnecessary rendering.

Using

If a recoil value is not available the hook will suspend drawing. A parent can then draw the shimmered version of the component or display “Loading…” :-)

Here’s an example of a component using recoil. This component does not need to deal with unavailable data directly, the parent is notified through the Suspense signal to draw a shimmer version. This greatly simplifies a component’s logic.

object MyComponent {
	trait Props exends js.Object { ... }
	def apply(props: Props) = render.elementWith(props)
	val render: ReactFC[Props] = props => {
		val recommendationsR = useRecoilValue[RecommendationsArray](recommendationsSelector)
		val (selected, setSelected) = useRecoilState(recommendationSelections)
		div(...)
	}
	render.displayName("MyComponent")
}

If an error occurs in the recoil graph, a react ErrorBoundary can catch it.

If you want a non-Suspense version, you can change useRecoilValue to useRecoilValueLoadable. It will return a loading, error and data type object similar to what you might expected from apollo graph. Note that apollo graph does not have Suspense support yet. If you use Loadable, then you need to query the loadable about its state. The recoil API covers all the combinations, each combination gets an API call. See the next section.

Javascript & Scala Effects Syntax Support

You can see that as javascript libraries become more aware of effects and asynchronous composition, the javascript APIs struggle to keep up. Here is part of the recoil API that shows the different ways to get a value from a Loadable . This definition comes from the overall facade I put together (took about an hour total since I had to read “flow” javascript).

@js.native
trait Accessors[+T] extends js.Object {
  /** Throw promise or error, or return the value. */
  def getValue(): T = js.native
  def toPromise[U >: T](): LoadablePromise[U] = js.native
  def valueMaybe(): js.UndefOr[T] = js.native
  def valueOrThrow(): T = js.native
  def errorMaybe(): js.UndefOr[js.Error] = js.native
  def errorOrMaybe(): js.Error = js.native
  def promiseMaybe(): js.UndefOr[js.Promise[T]] = js.native
  def promiseOrThrow(): js.Promise[T] = js.native
}

Clearly, a bifunctor design that could return an error channel and a value would be very useful here. zio’s bifunctor design would be the only effect model that would make sense here as an effect based solely on a scala Throwable (even a scala.js Throwable) could not ergonomically capture the full range of states/values.

Next Steps

recoil is new and the API in some places is still evolving. There is more work to be done. Having said that, the library works quite nicely and it allowed me to remove around ~70 lines of code from a few different modules and consolidate the logic into a “Graph.scala” file. I like that.

That’s it!

Comments

Popular posts from this blog

quick note on scala.js, react hooks, monix, auth

zio environment and modules pattern: zio, scala.js, react, query management

user experience, scala.js, cats-effect, IO