Search code examples
reactjscachingscala.jsscalajs-react

React Simple Global Entity Cache instead of Flux/React/etc


I am writing a little "fun" Scala/Scala.js project.

On my server I have Entities which are referenced by uuid-s (inside Ref-s).

For the sake of "fun", I don't want to use flux/redux architecture but still use React on the client (with ScalaJS-React).

What I am trying to do instead is to have a simple cache, for example:

  • when a React UserDisplayComponent wants the display the Entity User with uuid=0003
  • then the render() method calls to the Cache (which is passed in as a prop)
  • let's assume that this is the first time that the UserDisplayComponent asks for this particular User (with uuid=0003) and the Cache does not have it yet
  • then the Cache makes an AjaxCall to fetch the User from the server
  • when the AjaxCall returns the Cache triggers re-render
  • BUT ! now when the component is asking for the User from the Cache, it gets the User Entity from the Cache immediately and does not trigger an AjaxCall

The way I would like to implement this is the following :

  • I start a render()
  • "stuff" inside render() asks the Cache for all sorts of Entities
  • Cache returns either Loading or the Entity itself.
  • at the end of render the Cache sends all the AjaxRequest-s to the server and waits for all of them to return
  • once all AjaxRequests have returned (let's assume that they do - for the sake of simplicity) the Cache triggers a "re-render()" and now all entities that have been requested before are provided by the Cache right away.
  • of course it can happen that the newly arrived Entity-s will trigger the render() to fetch more Entity-s if for example I load an Entity that is for example case class UserList(ul: List[Ref[User]]) type. But let's not worry about this now.

QUESTIONS:

1) Am I doing something really wrong if I am doing the state handling this way ?

2) Is there an already existing solution for this ?

I looked around but everything was FLUX/REDUX etc... along these lines... - which I want to AVOID for the sake of :

  • "fun"
  • curiosity
  • exploration
  • playing around
  • I think this simple cache will be simpler for my use-case because I want to take the "REF" based "domain model" over to the client in a simple way: as if the client was on the server and the network would be infinitely fast and zero latency (this is what the cache would simulate).

Solution

  • Consider what issues you need to address to build a rich dynamic web UI, and what libraries / layers typically handle those issues for you.

    1. DOM Events (clicks etc.) need to trigger changes in State

    This is relatively easy. DOM nodes expose callback-based listener API that is straightforward to adapt to any architecture.

    2. Changes in State need to trigger updates to DOM nodes

    This is trickier because it needs to be done efficiently and in a maintainable manner. You don't want to re-render your whole component from scratch whenever its state changes, and you don't want to write tons of jquery-style spaghetti code to manually update the DOM as that would be too error prone even if efficient at runtime.

    This problem is mainly why libraries like React exist, they abstract this away behind virtual DOM. But you can also abstract this away without virtual DOM, like my own Laminar library does.

    Forgoing a library solution to this problem is only workable for simpler apps.

    3. Components should be able to read / write Global State

    This is the part that flux / redux solve. Specifically, these are issues #1 and #2 all over again, except as applied to global state as opposed to component state.

    4. Caching

    Caching is hard because cache needs to be invalidated at some point, on top of everything else above.

    Flux / redux do not help with this at all. One of the libraries that does help is Relay, which works much like your proposed solution, except way more elaborate, and on top of React and GraphQL. Reading its documentation will help you with your problem. You can definitely implement a small subset of relay's functionality in plain Scala.js if you don't need the whole React / GraphQL baggage, but you need to know the prior art.

    5. Serialization and type safety

    This is the only issue on this list that relates to Scala.js as opposed to Javascript and SPAs in general.

    Scala objects need to be serialized to travel over the network. Into JSON, protobufs, or whatever else, but you need a system for this that will not involve error-prone manual work. There are many Scala.js libraries that address this issue such as upickle, Autowire, endpoints, sloth, etc. Key words: "Scala JSON library", or "Scala type-safe RPC", depending on what kind of solution you want.


    I hope these principles suffice as an answer. When you understand these issues, it should be obvious whether your solution will work for a given use case or not. As it is, you didn't describe how your solution addresses issues 2, 4, and 5. You can use some of the libraries I mentioned or implement your own solutions with similar ideas / algorithms.


    On a minor technical note, consider implementing an async, Future-based API for your cache layer, so that it returns Future[Entity] instead of Loading | Entity.