0

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).
jhegedus
  • 20,244
  • 16
  • 99
  • 167

1 Answers1

2

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.

Nikita
  • 2,924
  • 1
  • 19
  • 25
  • Dear Nikita, thank you for your detailed answer, sorry for the late reply. I was a bit off the grid for a while, please allow me a few days to catch up with your answer and read it properly, and understand it. Many thanks again and sorry for the slow "reaction time". – jhegedus Dec 27 '18 at 17:17
  • "Caching is hard because cache needs to be invalidated at some point, on top of everything else above.' => YES. There are only two big problems in computer science :) => Fantastic ! Thank you for point me towards Relay ! Will look into that :) Cache invalidation is the main goal of FRP too. – jhegedus Dec 29 '18 at 13:59
  • "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. " => Yes, I wrote my "own" type safe RPC :) lol ... it's in the code above.... – jhegedus Dec 29 '18 at 14:33
  • Wow, https://github.com/raquo/Laminar, this is interesting. I have already seen it, starred it. It's like reflex for Haskell, the way I see it. I would use it but the react ecosystem is huge so... interesting post : https://github.com/raquo/Laminar/blob/master/docs/Virtual-DOM.md. – jhegedus Dec 29 '18 at 14:41
  • 1
    I was into FRP a lot, made a presentation about it some point too : https://www.youtube.com/watch?v=CjEDmJMLEGE ... :) very basic ... but.. it's OK :) ... I think one of the questions there came from `Cycle.js` author :) that was the time when `Cycle.js` was in it's incubation phase. – jhegedus Dec 29 '18 at 14:43
  • 1
    I think "Cache invalidation" is the hardest part... IMHO that is what FRP is all about. There is no invalid cache. You describe a state machine with FRP. No cache. So ... some sort of dependency graph needs to be maintained at runtime. Which Entity/View was generated from what... I guess I could do something like that with some funny "monad" on the server side :) but ... first things first ... :) – jhegedus Dec 29 '18 at 14:52
  • 1
    @jhegedus For data fully contained within an FRP system, FRP dependency chart can be seen as a perfect cache invalidator, yes. However, there's another aspect to this worth noting – data usually originates from and is later written to the backend database, which is normally external to the FRP system used on the frontend. So you need some special measures to notify your FRP system about these external changes to make sure the values on the frontend are always valid. Could be websockets for example. – Nikita Dec 31 '18 at 05:40
  • Yes, indeed, of course, but it is just an interesting point of view to look at FRP as a cache invalidator and use it as some sort of inspiration for creating some sort of simple "async" cache invalidator. Indeed, this is not a simple question, how to do this right. There are two ways: 1) explicit, static (+type safe) declaration of dependency graph (which mostly should only exists on the server) => deriving some invalidator => this is the FRP-like approach, 2) run time dependency graph generator, using some sort of custom monad. I am sure some sort of "prior art" exist for both approaches. – jhegedus Jan 03 '19 at 15:59