Hooks are relatively new in the react world. Hooks provide slices of capabilities to a component in a more composible model than class-based components. A “fetcher hook” fetches data and provides it to a react function component asynchrously as the data becomes available. The component must already be rendered to use a fetcher hook.

I created a general scala.js react fetcher component in a previous blog. It works, but it is not a hook fetcher. Because it is not a hook, it is less flexible because it forces the application to use a specific component hierarchy. The fetcher in the blog is a react component–it returns a “dom” component. A react hook is not a component–a hook returns data and functions.

The NPM registry is full of js/typescript react hook fetchers. They are easy to write. If a js Promise works for you, use them. Most hook implementations assume you are using HTTP transport via the browser’s “fetch” API and “url” state model. async/await was recently added to javascript making it easier to use js.Promise effects compared to the standard hierarchical, callback tree syntax. Today, many programs use non-HTTP protocols such as websockets or rsocket (websockets with advanced protocols). While useful today, some of the react hook fetcher libraries may be less useful going forward.

The recent introduction of react “Suspense” has raised awareness of how rendering imacts the user experience. If you are suddenly shown a large spinner screen while data is being fetched, then flip to a new screen, the user may feel that there were too many flips and become distracted. Many fetcher libraries do not address this probem. React’s “concurrent” rendering support allows a react component to start rendering, easily inform the rendering engine that it cannot continue to render, allow the rendering engine to render other components, then render the component once an asynchronous “condition” is satisfied. Instead of trying to expose a complicated API, FB wants to make this easy to use.

Here’s some links for react, async data fetching. You may want to review them because they give you a hint about asynchronous fetching features that are useful in real applications. You will want to keep an eye on these features as you work through this blog.

Suspense interrupts a component’s execution path and allows other parts of the UI to render. You set the alternative display logic in the parent component using a Suspense component. If the child’s render is interrupted, the fallback rendering logic is used. The child interrupt’s itself by throwing a js.Promise when it has a dependency that prevents it from rendering. The dependency is often, but not always, a data dependency. There can also be a “code” dependency via the new async import mechanism.

By using a “Suspense” component, you can avoid adding excessive machiney in the parent and child. FB has updated relay with Suspense . relay focuses on graphql and advanced caching models. Unsurprisingly, relay is a complex library with code generators, compiler augmentation, string-based DSLs and alot of abstractions. To solve these problems while keeping it ergonomic for programmers, you may need industrial grade software engineering. Once you look at relay, you immediately wonder if there is an easier way–a subject for another blog.

Old fetcher component

Back to my poor, old fetcher component…

The code below is from the original blog with some minor changes. You must provide a “runner” to run the scala effect F . The strategy for running an effect is captured in a parameter. Separating out the “runner” makes the component simpler and more reusable because you can use any “runner” strategy as long as it produces a non-effectful Either . An Either is a common scala data structure that provides 2 values. In our case, the value is either a domain value or an error value. Unlike other react component libraries, there is no assumption about the effect. The scala.js component could work with a websocket or an rsocket. As an aside, neither the component function or the hook described later are pure functions due to the way that react works.

class Fetcher [ F [ _ ] , P , E , T ] ( Name : String ) { sealed trait FetchState case class Success ( item : T ) extends FetchState case class Error ( content : E ) extends FetchState case object Fetching extends FetchState case object NotRequested extends FetchState type FetchCallback = F [ P ] => Unit type Runner = F [ P ] => ( Either [ E , T ] => Unit ) => Unit trait Props extends js . Object { val children : ( FetchState , FetchCallback ) => ReactNode val run : Runner val initialValue : Option [ F [ P ] ] } def apply ( props : Props ) = sfc ( props ) val sfc = SFC1 [ Props ] { props => import props . _ React . useDebugValue ( Name ) val ( fstate , setFState ) = React . useStateStrictDirect [ FetchState ] ( NotRequested ) val makeRequest = React . useCallback [ F [ P ] , Unit ] ( fstate . asJsAny ) { f => if ( fstate != Fetching ) { setFState ( Fetching ) props . run ( f ) { _ match { case Right ( item ) => setFState ( Success ( item ) ) case Left ( e ) => setFState ( Error ( e ) ) } } } } children ( fstate , makeRequest ) } }

The component can be inconvenient to use. I eventually defined a subclass of Fetcher that curried the runner argument to avoid passing it in every call. The signature has a P narrowed to a T . Narrowing is not needed. You could just manipulate the final value in your application code vs the runner callback. So T can be removed. I’m not sure why I did that in the initial version. The code also does not you to show a child component with old data while informing the child that the data will change soon unless you cache the data in the child.

The component is based on the scalajs-reaction library, a scala.js library that relies exclusively on react hooks. You could write your own scala.js hooks facade in about ~50 lines of code.

Fetcher Hooks

A react hook does not return a react node. A hook returns data and functions that allows the caller, a function component, to manage rendering.

The hook verison is nearly identical to the component version–less the drawing code. The hook is inside the class expressed as a scala function. However, the hook function can only be called inside a react function component. There is no clever programming way that restricts the function so it can only be called inside a function component.

To use the hook, instantiate a FetcherHook then call the class’s useFetcher in your function component. All instances will use the same effect runner. If we added a mutable, application-level cache to the class as constructor parameter, all instances would could use the same cache. Of course, FP-style programming would want to use an immutable data structure so passing in the cache directly would not reflect good FP practices.

Ideally, we would push the result type P into the actual hook function so it could return different types of data. A nice feature of the hook is that w cannot use FetchState instances’s across different instances of FetcherHook. FetchState is a path dependent type so MyFetcherHook1.Success is not the same as MyFetcherHook2.Success.

class FetcherHook [ F [ _ ] , P , E ] ( Name : String , runner : F [ P ] => ( Either [ E , P ] => Unit ) => Unit ) { sealed trait FetchState case class Success ( item : P ) extends FetchState case class Error ( content : E ) extends FetchState case object Fetching extends FetchState case object NotRequested extends FetchState type FetchCallback = F [ P ] => Unit def useFetcher ( ) => { React . useDebugValue ( Name ) val ( fstate , setFState ) = React . useStateStrictDirect [ FetchState ] ( NotRequested ) val makeRequest = React . useCallback [ F [ P ] , Unit ] ( fstate . asJsAny ) { f => if ( fstate != Fetching ) { setFState ( Fetching ) runner ( f ) { _ match { case Right ( item ) => setFState ( Success ( item ) ) case Left ( e ) => setFState ( Error ( e ) ) } } } } ( fstate , makeRequest ) } }

Here’s the hook in action:

val Fetcher = FetcherHook [ IO , MyResponse , Throwable ] ( "MyFetcher" , CatsFetcher . runner [ IO , MyResponse ] ) import Fetcher . _ object MyComponent { trait Props extends js . Object { } val sfc = SFC1 [ Props ] { props => val ( fstate , doFetch ) = useFetcher ( ) fstate match { case Success ( item ) => div ( s "item fetched: $item" , button ( . . . on click call doFetch . . . ) ) case Error ( t ) => div ( s "Error $t" ) . . . } }

The cats runner is a bit complicated because it uses IO as in intermediary effect. zio is a bit cleaner.

object CatsFetcher { def runner [ F [ _ ] : Effect , T ] : F [ T ] => ( Either [ Throwable , T ] => Unit ) => Unit = { val F = Effect [ F ] f => eicb => F . runAsync ( f ) { asyncei => F . toIO ( F . pure [ Unit ] ( eicb ( asyncei ) ) ) } . unsafeRunSync ( ) } }

We could use CatsFetcher with zio by using the zio cats interop library but its cleaner using zio directly:

object ZioFetcher { def runner [ T ] ( rts : Runtime [ Any ] ) : Task [ T ] => ( Either [ Throwable , T ] => Unit ) => Unit = f => eicb => rts . unsafeRunAsync ( f ) ( e => eicb ( e . toEither ) ) }

Unlike JVM applications, there is no main entry point so we cannot return an effect to an “App” subclass that runs the “application” effect on our behalf. In the browser, the effect runtime must run the effect based on the react component’s lifecycle. There leads to many “unsafeRunAsync” method calls throughout the code.

More Goodness

The fetcher above only remembers one piece of data at a time but you may want to use a cache so it serves up multiple dependencies to the same component. In the example above, you would need three separate useFetecher() statements in your component if there were three data dependencies.

A unified cache using a data key would reduce the need for three useFetecher() statements and could also support returning stale data immediately with an “request-in-flight” flag indicating a request for fresh data has been made. “relay”'s support (https://relay.dev/docs/en/experimental/a-guided-tour-of-relay#reusing-cached-data-for-render) for these features suggest that it would be useful. Relay defines a customizable “Environment”, say with caching. The Environment is the first argument to most relay API functions suggesting that a Reader/State monad formulation might also be useful! As a side note, the “store” API looks like the old ADO.NET interface for navigating master-detail relationships…what is old is new.

Existing fetcher libs like https://github.com/bghveding/use-fetch orhttps://github.com/jamesplease/react-request could use https://github.com/jamesplease/fetch-dedupe to dedupe requests. To use dedupe, you would just call fetchDedupe vs fetch directly. That’s insanely easy unless the fetch call is inside the library and you cannot customize it. These libraries are specific to the browser but in some cases can be run on the server for server-side rendering. Libraries vary in configurability, e.g., using a shared cache across the application or narrowing the cache scope to a few pages. A browser’s Response object is not idempotent so there is also a bit finesse needed, e.g., you cannot call response.json() more than once.

With zio, we could call .memoize (or .cached ) on your effect and it will memozie the response for you without any further cache/fetch/other configuration or coding. You would need to add some boilerplate to your component to store the “value” and reuse it when appropriate. .memoize is not quite right though as you would not want to memoize an error value, but in spirit, using effeects directly could be useful.

We could use zio to implement “return a result immediately but fetch in the background” semantics. Perhaps the the only value the hook provides is te ability to force a redraw as a fetch progress in its states. The hook also transforms the return value into two either an error value or a domain value. With zio, we can explicitly expose a customized error channel so in theory we could drop the Either.

Let’s break down our problem. Assuming the runner is separate, the hook provides:

Storage for the return value and fetch status. Fine grained concepts of fetch “state” which changes as the fetch lifecycle advances, e.g., NotRequested -> Fetching -> Success. Component notification when that state changes and it needs to redraw.

We need storage for (1). We need storage for (2). We need something react’ish for (3). For (3), given the react API, we can use setState to set the state and force a rerender–we cannot avoid this aspect. We might as well store the state of the fetch in a react state hook so its convenient to access. We may also choose to store the fetch state elsewhere so we can manage the fetching lifecycle more cleverly but we will always need at least one hook. To force a rerender in react, you need to change state and state storage requires a “state” hook. So (3) will come from a React.setState hook. But (1) and (2) are open to change.

zio

There are some good scala FP libraries that could help with (1) and (2), fetch in particular was designed to abstract the fetching process, regardless of protocol, and dedupes feches and caches results. It has other features such as automatically batching results. In a way, fetch is a spiritual cousin to relay. However, fetch or relay may be too much abstraction for your app or you it may not be appropriate for your app, e.g., you are not using graphql. Also, fetch does not provide all of the capabilities you may need, witness the capabilities of the fetch libraries mentioned in the first section. Perhaps with zio we can use some zio features to incrementally enhance fetching. Ideally, the solution can be used anywhere, the browser or the server.

For (1) and (2) we can use some zio capabilities similar to what is described https://zio.dev/docs/overview/overview_testing_effects and here to provide a cache to an effect in the zio “environment.” The scope of the cache the entire application or a few web pages based on the enivronment value. Instead of a “Any” environment, we could use an enviornment that has a “Cache” service. In the formulation below, the scope of the cache can be controlled by using an environment shared among all components or you could create a new environment with a fresh cache specific to a page.

For illustratiation purposes since not everyone uses scala.js, we can make the service work on the JVM or in the browser by using a “Ref” data structure. A “Ref” controls access to its content and is safe to use in a multi-threaded environment. Hence, the version below is more complicated because it is built for both environments. I’ll publish the much shorter and simpler version in another blog or a github repo.

Admitedly, if we were just sticking with HTTP calls, we could use one of the packages mentioned above to easily employ a cache. To be fair, assuming the HTTP response contains the right headers, the browser will automatically cache the content. However, even if the browser caches the response, we still incur json parser processing overhead. Caching will increase memory usage but decrease processing needs–the classic cache trade-off. Let’s ignore all of these design dimensions for the moment and just setup a zio based caching capability specific to a “fetching” concept.

FP style programming uses immutable data and effects-as-values. Once you decide on the FP-style, you are immediately thrust into a world where you write your logic inside the effect. On the JVM, zio has the zio.App class you use for your main class. zio.App expresses a main function that expects an effect as the return value. In the browser, there is no “main.” We need to wrap the react “render to DOM” in an effect in order to provision a global cache data structure if the cache itself is inside an effect like “Ref”

There are extensive comments in the code below so you can read the code and comments to understand how it is designed and why. I know that excessive comments can make code hard to read but I thought this was the best way to go.

You should also notice the nearly complete lack of type specifications except for the function declaration. zio is designed to work with scala’s type system. zio type inference is good and type specifications are seldom needed. Since its just the zio effect, there is no F[_] type parameters.

I had nearly 0/zero/zilch type inference issues and the compiler always clearly expressed type problems so writing my code was quite easy once I understood the zio API.

We define the caching system designed as a “module” using the pattern described on the zio site. We also rename the “fetch” concept to one of “data dependency”. There are various equivalent ways to declare the GADT for the data dependency information so if you don’t like the way DataDependency is structured, you can change it. All of this code would be in a library that you never see if you are just consuming the capability.

sealed trait DataDependency [ + T ] case class Available [ + T ] ( data : T , inflight : Boolean , cache : Boolean ) extends DataDependency [ T ] case class Error ( m : String ) extends DataDependency [ Nothing ] case object InFlight extends DataDependency [ Nothing ] case object NotRequested extends DataDependency [ Nothing ] trait FetchingSystem { val fetching : FetchingSystem . Service } object FetchingSystem { trait Service { def clearData ( key : String ) : UIO [ Unit ] def put ( key : String , last : Available [ _ ] ) : UIO [ Unit ] def get [ T ] ( key : String ) : UIO [ Option [ Available [ T ] ] ] def status [ T ] ( key : String ) : UIO [ Option [ DataDependency [ T ] ] ] def setStatus [ T ] ( key : String , status : DataDependency [ T ] ) : UIO [ Unit ] } case class DefaultService ( state : zio . Ref [ Data ] ) extends Service { def clearData ( key : String ) = state . update ( data => data . copy ( lastSuccessful = data . lastSuccessful - key ) ) . unit def put ( key : String , last : Available [ _ ] ) = state . update ( data => data . copy ( lastSuccessful = data . lastSuccessful . updated ( key , last ) ) ) . unit def get [ T ] ( key : String ) = state . get . map ( _ . lastSuccessful . get ( key ) . map ( _ . asInstanceOf [ Available [ T ] ] ) ) def status [ T ] ( key : String ) = state . get . map ( _ . current . get ( key ) . map ( _ . asInstanceOf [ DataDependency [ T ] ] ) ) def setStatus [ T ] ( key : String , status : DataDependency [ T ] ) = state . update ( data => data . copy ( current = data . current . updated ( key , status ) ) ) . unit } def makeDefault ( data : Data ) = zio . Ref . make ( data ) . map { s => new FetchingSystem { val fetching = fromRef ( s ) } } def fromRef ( ref : Ref [ Data ] ) = DefaultService ( ref ) def clearData ( key : String ) = ZIO . accessM [ FetchingSystem ] ( _ . fetching . clearData ( key ) ) def put ( key : String , last : Available [ _ ] ) = ZIO . accessM [ FetchingSystem ] ( _ . fetching . put ( key , last ) ) def get [ T ] ( key : String ) = ZIO . accessM [ FetchingSystem ] ( _ . fetching . get [ T ] ( key ) def status [ T ] ( key : String ) : ZIO [ FetchingSystem , Nothing , Option [ DataDependency [ T ] ] ] = ZIO . accessM ( _ . fetching . status [ T ] ( key ) ) def setStatus [ T ] ( key : String , status : DataDependency [ T ] ) : RIO [ FetchingSystem , Unit ] = ZIO . accessM ( _ . fetching . setStatus [ T ] ( key , status ) ) case class Data ( lastSuccessful : Map [ String , Available [ _ ] ] = Map . empty , current : Map [ String , DataDependency [ _ ] ] = Map . empty ) }

There are a couple of casts in there that we could get with at the expense of making the API more complex. We’ll skip fixing the casts as as an objective for the moment. We need to define what our environment will look like:

object AppRuntime { type AppEnv = FetchingSystem with clock . Clock case class Env ( fetching : FetchingSystem . Service ) extends FetchingSystem with clock . Clock . Live val refInEffect = zio . Ref . make ( FetchingSystem . Data ( lastSuccessful = Map ( "blah" - > Available [ String ] ( "hah" , false , true ) ) ) ) val rts = new DefaultRuntime { } }

In order to use the cache system, we need to write some code that structures the processing and implements our logic. The standard approach with effects is to use a for-comprehension. A for-comprehension requires that all of the RHS have a compatible monad. In scala, this means they must have map, flatMap, withFilter and similar data types–you can’t mix monad types in the same for-comprehension although you can break up the for-comprehension into multiple for-comprehensions.

For us, the RHS of the for-comprehension will be ZIO monads.

val program = for { aValue <- . . . a zio effect . . . _ <- . . . another zio effect . . . bValue <- . . . a zio effect . . . } yield ( )

The trick to using zio for-comprehensions is to define methods for use on the RHS that are ergonomic, access the cache, then access the environment for services (like dependency injection) and call the service methods to perform “caching/fetching” logic. If we are using zio effects, the environment, and hence the ZIO effect type, must match so that the for-comprehension will compile. If we have effects with different environment needs, we will need to use .provide(env) inside the for-comprehension at different times to standardize the environment types to product RHS effect values with the same type. You should recognize that mixing zio.Task and zio.ZIO[MyEnv,...,...] types can result in a bit of a nuanced dance to ensure you have the correct environment dependencies.

I will also make the disclaimer that the cache described below is insufficient for general use. We would want to bake in content expirations and other nifty cache features. There are some js and scala general purpose caches that could provide this and we could just use those instead of a simple Map.

In addition, we have customized the cache contents to the lifecycle of a remote call and we have provided a “logging” capability that allows us to log state changes. The “logging” allows us, conveniently, to tie into the react component lifecycle. We would probably also want a way to turn off caching and clear the cache if the routing changed, etc. Just see the relay documentation for more ideas. The cache below just shows the basics of using a cache to reduce dedupe and improve the user experience. The code would be bundled into a library so a consumer would never need to see it.

object Requests { import AppRuntime . _ import FetchingSystem . _ def withCache [ T ] ( effect : Task [ T ] , key : String , log : DataDependency [ T ] => Task [ Unit ] , policy : CachePolicy = CachePolicy . NetworkOnly , ) : ZIO [ FetchingSystem , Throwable , DataDependency [ T ] ] = ZIO . accessM { r => def networkFetch ( skipLog : Boolean ) = { val update = ( if ( skipLog ) Task . unit else log ( InFlight ) ) * > setStatus ( key , InFlight ) ( update . provide ( r ) * > effect . foldM ( e => Task . succeed ( Error ( e . getMessage ) ) , d => { val s = Available ( d , false , false ) put ( key , s ) . provide ( r ) * > Task . succeed ( s ) } } ) for { lastopt <- get ( key ) statusopt <- status ( key ) result <- ( lastopt , statusopt , policy ) match { case ( _ , _ , CachePolicy . NetworkOnly ) => networkFetch ( false ) case ( _ , Some ( x @Available ( s , i , c ) ) , CachePolicy . CacheFirst ) => if ( ! i && c ) Task . succeed ( x ) else Task . succeed ( x . copy ( inflight = false , cache = true ) ) case ( Some ( last ) , Some ( InFlight ) , CachePolicy . CacheFirst ) => Task ( last . copy ( inflight = true , cache = true ) ) case ( Some ( last ) , _ , CachePolicy . CacheFirst ) => Task ( last . copy ( inflight = false , cache = true ) ) case ( _ , Some ( x @Available ( s , i , c ) ) , CachePolicy . CacheAndNetwork ) => networkFetch ( true ) * > Task . succeed ( x ) case ( Some ( last ) , Some ( InFlight ) , CachePolicy . CacheAndNetwork ) => if ( last . inflight && last . cache ) networkFetch ( true ) * > Task . succeed ( last ) else networkFetch ( true ) * > Task . succeed ( last . copy ( inflight = true , cache = true ) ) case ( Some ( last ) , _ , CachePolicy . CacheAndNetwork ) => if ( last . cache ) networkFetch ( true ) * > Task . succeed ( last . copy ( inflight = false ) ) else networkFetch ( true ) * > Task . succeed ( last ) case _ => networkFetch ( false ) } _ <- log ( result ) * > setStatus ( key , result ) } yield result } val appenv = refInEffect . map ( FetchingSystem . fromRef ( _ ) ) . map ( Env ( _ ) ) def mkEnv ( dataRef : Ref [ FetchingSystem . Data ] ) = Env ( FetchingSystem . fromRef ( dataRef ) ) }

That’s the core of the system. But now we need to set it up for use in the browser. First, we need to define a couple of helpers to work with the browser fetch API:

import scala . scalajs . js import org . scalajs . dom . experimental . _ import scala . util . chaining . _ import scala . scalajs . runtime . wrapJavaScriptException def jsPromiseToZIO [ T < : js . Any ] ( p : js . Thenable [ T ] ) = Task . effectAsync [ T ] { cb => p . `then` [ Unit ] ( { ( t : T ) => cb ( Task . succeed ( t ) ) } , js . defined { ( e : scala . Any ) => cb ( Task . fail ( wrapJavaScriptException ( e ) ) ) } ) } def zioGet [ T < : js . Any ] ( url : String ) = Task . effectSuspendTotal ( ( Fetch . fetch ( url ) pipe jsPromiseToZIO ) . flatMap ( _ . json ( ) pipe jsPromiseToZIO ) . map ( _ . asInstanceOf [ T ] ) )

Using the zio “fetching” environment

To use a global cache, we need to wrap our entire program inside an effect that holds the cache–we need to unwrap the value from the effect. Essentially, our “main” program becomes a giant callback inside the Ref’s effect via the standard scala .map method.

Once we are in the effect and can access the Ref, we create a react context to allow other components to access the Ref. Each component wishing to use the application wide cache needs to tap into the context, obtain the Ref[Data] instance, create an environment, then run their effects using that environment. In the example below, the shared cache is put into the context but the environment itself could also be placed directly into the context–its an application dependent decision.

Here’s the wrapped “main” entry point. You can see that we are performing the top level rendering into the dom inside the effect after running that effect with default runtime system.

case class ZioContext ( cache : zio . Ref [ FetchingSystem . Data ] ) object Main { val Context = ttg . react . context . create [ ZioContext ] ( null ) @JSExportTopLevel ( "App" ) { def App ( options : js . Object ) : Unit = { AppRuntime . rts . unsafeRunAsync_ ( AppRuntime . refInEffect . map { ref => reactdom . renderToElementWithId ( Context . provider ( ZioContext ( ref ) ) ( Application ( ) ) ) , targetContainerElement ) ) } ) } }

Let’s define a component that accesses the context, creates an environment and runs the data-fetching effects using the “withCache” method.

import AppRuntime . _ import Requests . _ type RT = api . APIArrayResponse [ DomainObject ] object MyComponent { val Name = "MyComponent" val getlist = zioGet [ RT ] ( s "${BuildConstants.endpoint}/domainobjects" ) trait Props extends js . Object { . . . } val sfc = SFC1 [ Props ] { props => val zctx = React . useContext [ ZioContext ] ( Main . Context ) val env = mkEnv ( zctx . cache ) val ( fstate , setFetchState ) = React . useStateStrictDirect [ Fetch2 [ RT ] ] ( NotRequested ) val cached = withCache [ RT ] ( getlist , Name , status => Task ( setFetchState ( status ) ) , CachePolicy . CacheAndNetwork ) . provide ( env ) React . useEffectMounting { ( ) => rts . unsafeRunAsync ( cached ) ( exit => println ( s "mounting exit $exit value not used" ) ) } fstate match { case Available ( data , inflight , cache ) => if ( inflight ) PageWithDataIndicateFetching ( data ) else PageWithData ( data ) case InFlight => ShowLoading ( ) case Error ( e ) => ShowError ( e ) case _ => PageWithoutData ( ) } } }

The “log” function was used to tie into the react state machinery. As the “status” of the fetch changes, the component is notified via a state change and react redraws the component. You will notice that we now have 2 function calls to use the fetching system, one for the context and one for state. Its a really important point that all of the asynchronous machinery is independent of the actual function component. In order for the effect to notify the drawing machinery, a simple “log” function was used to set the react state that then forces a re-render. It’s an important decoupling design: the caller chooses how to inform the component how to render. We build up “features” of the asynchronous call by using zio combinators which are composable. We could also formulate the entire approach into a hook to make using this much easier. I do not do the above process in every component, I just use a hook passing in the key and effect.

Since the caching function is just a higher-level service we could also just do:

trait DataManagement { val dm : DataManagement . Service } object DataManagement { trait Service { def cache [ T ] ( key : String , policy : CachePolicy ) ( effect : => Task [ T ] ) : ZIO [ CacheSystem , Throwable , Dependency [ T ] ] } }

and use the “withCache” as a service via the data management “service” available in the environment. There are actually multiple, othogonal services we could be using to run queries that are not used in the above formulation.

Continuing with the formlation prior, here’s how the core part would look using a hook (the hook is not shown). We would just call “run” to run the fetch.

val ( fstate , run , zenv ) = hooks . useZio [ ResultType ] ( s """${name}_entity_home_view_${state.searchText.getOrElse("na")}""" , fetchItems ( state . searchText . toOption ) , rts = rts )

Of course, accessing the rts like this is a bit crude, we would really just put the entire environment into the react context and create a service API that access the “client” and the “rts” as needed. We could also use zio environments with “query parameters” baked in so that there is no need to call fechItems(state.searchText.toOption) like above, but access a query specific environment to obtain the query parameters.

Wrapping Up

The js world is complicated because of the tendency for each library to provide a sliver of effects management. For example, if you look at this [package](https://github.com/slorber/awesome-debounce-promise for debouncing js Promises, you realize that to have other effect features, you need yet another library which only composes through react components, at best. At least with zio, its all in one place. And you might to use https://github.com/axios/axios just to create the initial effect.

In the formulation above, we could get rid of the context usage simple be evaluating the main application like before but setting a global, mutable variable and accessing that instead of setting up a context. In the single threaded js world, that’s Ok to do. You would not use that approach in the JVM world.

Fetching is changing in the react world. React “suspense” helps with creating a smoother data interface (example). It uses a cache behind the scenes to provide storage for asynchronous operations. You pragmattically need an application wide cache when using Suspense in order to store a result even if the component is not currently rendered. FB published an application-wide cache js package for use with Suspense which throws a Promise when the data is accessed but not available. I’ve used a variant of the above formulation with the new react suspense mechanism to delay drawing until data is read and push display logic up to the parent instead of down into the tree while still allowing the data dependencies to live in the child.

However, its clear that the entire idea around Suspense fetching is quite a small improvement in rendering. You can just start fetching the data as soon as possible, prior to rendering the consuming component if possible, and use one of a few different mechanisms to conditionally render when the data is ready. You do not need to throw a js.Promise or use a specific component (Suspense) to do that.

The above fetcher formulation is more complicated than just loading a package and using “fetchWithCache” instead of “fetch”. It is also alot more complicated then the fetcher hook–you had to alter your “main” program! From a total lines of code count, its less or about the same as other packages. The level of abstraction is significantly higher and processing overhead may be or may not be higher. However, part of the complexity arises from the desire to use the same mechanism on both the JVM and browser. I could have used a mutable cache and made this much smaller. Using the approach above does allow you to implement a consistent, application wide effects management system–you could instrument/monitor all of your calls using this approach.

With zio, I can also do a bunch of other tricks using a wide range of combinators. Given fetching and other user experince enhancements requiring data fetching/asynchronous programming, the effects model in zio is helpful.

The zio approach above will work server side, browser side and across communication protocols–not just HTTP. In today’s world, HTTP-based web services are becoming less common as other protocols are significantly more efficient despite the presence of compression. This is especially true in places where there are lots of people, cheaper/older phones and less powerful infrastructure.

There are other ergonomic factors involved when creating a good UI. I covered some of these in another blog e.g. how to add delays to the fetch to improve the use experience and avoid display flipping. I will not cover that here again with zio as they are easy to implement.

That’s it!

Note: I’ll blog out a more production version of all of this for scala.js browser work in an upcoming blog. Once you realize the pattern is no different than what you have seen before and you get used the Reader monad and effects (and simplify things since you are in the browser), its amazingly nice how it all comes together. We can also use more features of zio to simplify th code while still retaining flexibility and type correctness. We do not actually leverage zio as much as we could in the above formulation. We can be much more clever.