Redex - Predictable Server Side State Containers - à la Redux for Elixir

Here is part of a proposal/idea for bringing server side state - I.E LiveView minus the view bit - into an SPA or whatever UI. I envision this working on mobile, web, maybe even Scenic, does not need to be coupled to JavaScript or Phoenix.

This is following up on the thread: Phoenix LiveView vs SPA - #48 by harmon25

I have played around a bit with the API and it is very doable - still some work to finalize it. Not in a currently working state - but should be soon.

I am sure people who are fully sold on LiveView and using it in its current state don’t really get why.
The flip side are all of us who have spent the last 5+ years mastering the JS stack/ecosystem, and just cant throw out the baby with the bathwater…

So, onto the real questions.

  1. Is this a good idea? :stuck_out_tongue:
  2. The API look OK?
  3. Foreseeable challenges or issues?

Thanks everyone who gave this a read!

10 Likes

So if I understood correctly you’d be able to use whichever frontend library you want and keep state on the server? That sounds very cool and could mean a bit more flexibility to select tools for a project.

1 Like

Yep, that’s the idea!

1 Like

I’m not sure what it would look like, but it would be interesting to see this integrated into redux as a proxy that reflects the state and updates it based on changes (from Channels). Bonus points if it can be combined with an existing store so that you can have some client side redux state if necessary (not everything is serializable).

I’m a huge fan of redux, and I think that a lot of interesting use cases can come from separating your store across boundaries. An example of this is the webext-redux library, which allows for redux state to live across a Chrome extension background/foreground boundary.

One use case that becomes trivial to implement if the redux state is synced with the server is that you could have 2 different pages staying in sync with each other by subscribing to the same state reference on the server.

2 Likes

Have had this idea, and had a prototype that kept server and client state in sync via redux middleware on the client.

Am trying to avoid the state being simply replicated in the browser, but it’s kinda un-avoidable at a certain point(it’s atleast copied to the DOM)… pretty sure just simple object + onChange callbacks triggered by a server diff are enough for most reactive UI frameworks.

I am not really considering any JS components in Redex. I envision a smaller layer on top of Redex via phoenix channels + some JS that would go after that challenge, (see LiveData) and explore ideas that work best for a respective UI framewore IE React vs Vue integrations

Yes! This was very easily achievable with an earlier LiveData prototype, which was a motivation for Redex.

The non-serializable data is a bit of a challenge for sure - I think hydrating the non-serializable data structure - with something that is serializable is the best approach - if not an option - is something that must remain as client only state, either in a client only redux store - that does not have to tied at all to the backend - or some other client store - maybe just internal react state + refs? This is also a problem LiveView doesn’t really solve - was brought up in the other thread around large JS libs that have internal state.

1 Like

I have the POC working!
Take a look at the test for an idea of the API, including state composition via aggregate reducers, similar to redux’ combine reducers function, but reducers are not functions, they are modules - with action and default_state callbacks…

Process arch looks like this after launching a single store - with some nested reducers:

Which is a hierarchy like the vDOM LiveView essentially builds, but events go all the way down to the leaf nodes by default so do not need to worry about where the events are attached or anything - you just have to run a function in the UI dispatch({type: "add", payload: 5}) - that will do something to state, or not…

1 Like

Just chimming in to mention that this is a super interesting idea! :slight_smile:

I’ve been working around a similar use-case (ie a shopping cart via LiveView) by leveraging the updated event of LiveComponents. In there once the client receives an update it re-hydrates the clientside state(locastorage).
This is useful for restoring state for LV (fetched from localstorage -> sent as LiveSocket’s params)

Hoping for a good practice/standard approach for these types of problems. Looking forward to where these initiatives bring us :rocket:

Some questions to add to the discussion:

  1. I understand this is server-side so far, but would the dispatcher client be able to optimistically update in your idea of implementation?
  2. Can processes that are not coming from client (say external BE processes or 3rd parties). How would the architectural priority map look?

Thanks for checking it out!
I am considering optionally bootstrapping the store with some initial state that could be persisted on the client between sessions on IndexDB or localstorage.

  1. I don’t think so - as this requires implementing the reducer logic on both front and backend which defeats the objective of less code. If you have experience with phx sockets/channels you’ll know that unless its big payloads or large geographic distance - events can be pushed very quickly…like microseconds. This approach will only really work with a decent network - same constraint as liveview.

  2. So in order to do stuff on the backend - will likely need a GenServer or something (listening for external handle_info events etc) - that with access to the store pid would be able to dispatch actions on the backend - that are then pushed to the UI. Not sure what you mean by architectural priority - the store acts as a bottleneck for dispatched actions - ensuring they are handled serially…

Hope that answers your questions!

1 Like
  1. Agreed
  2. Yeah this is more a potential optimization, the very message queue of the GenServer is an excellent approach for most cases, agree as well :slight_smile:

initial state that could be persisted on the client between sessions on IndexDB or localstorage.

Great, that’s exactly what I’ve been doing to hydrate LiveSocket as mentioned above, but thinking about it if you keep the store saved and stored(duh) at the server, related to a user_id, then the client only needs to know that user_id and it becomes cheaper to kickstart it all to the latest state :slight_smile:

What is the benefit of having so many processes? Would it be enough to have one GenServer as the root reducer, and it taking modules that represent the data shape of the reducers with the same API (action/3, etc), thereby giving you 1 process (and a lot less message passing overhead) that uses a composable data structure?

I did think about this. It is an awful lot of processes for just a map.

There maybe a tiny concurrency benefit when ‘casting’ the action down the tree.(if it’s big enough)…but probably doesn’t outweigh the msg overhead. Also have not really tried, but failure may also be isolated…an action that crashes a reducer might some how be recoverable at only that level and below on the state tree…

Am considering @sasajuric ‘Parent’ to flatten that tree…each aggragate reducer is a genserver and a dynamic supervisor… So could remove atleast 3 processes from that tree I believe.

If my thoughts are wrong, would definately consider a simpler design…

Every stateful LiveView is a process, so if they can do it… :stuck_out_tongue:

Thanks for checking it out!

1 Like