Client-side rendering, server-side state (StateChannel)

LiveState seems the most mature of all the implementations of the general idea, though it lacks some nice features from Storex and StateChannel. Do you accept PRs?

I’d like to play with LiveState, but I can’t get the testbed-app running.

  • Install dependencies with mix deps.get
  • Start Phoenix endpoint with mix phx.server or inside IEx with iex -S mix phx.server

Now you can visit localhost:4000 from your browser.

these steps are not enough.

Yes, would be happy to take PRs. The testbed project primarily exists to facilitate automated end to end testing in CI via github actions, it may not be the best example project to look at. I have several other examples listed in the main README tho. Can you elaborate on some of the features from Storex and StateChannel you’d find useful?

these are for the webcomponents, do you have an react-example?

I did not really look into LiveState too much yet, so I do not know how you do it, but here are some points:

  • StateChannel provides the state in a Context which is very nice and easy to use
  • StateChannel allows you to either transform and then diff the state on the server or you apply the JSONpatch primitives manually using patch_state which collects the patches in an array which is then send over the wire and the same time patches the server state (using JSONPatch). This skips the JSONDiff step.
  • Storex store acts as a subscribable that can be consumed by most frameworks, eg using solid’s from.

I don’t just yet, I’m more interested in web components. In that case, checking out the test bed might be your best bet after all. It should run if you clone the live_state repo, going into the testbed directory and do the standard mix deps.get and mix phx.server. If not do me a favor and file and issue with what you are seeing so I can investigate.

My thought here is that a hook is the newer ‘preferred’ way for working with react. But be happy to take PRs

These are interesting, particularly the subscribable idea. I’ll look into that

1 Like

I tried that already, does not work, I created an issue. Also some minor issues in the comments-demo, which I have running. That’s really cool, I should look into that webcomponents thing again.

I think the differences, from what I see is that:

  • I have patch_state function, that allows user to selectively update the state and generate JSON Patch at the same time. In fact, the Patch is first generated, then applied on server, and sent to client to set the state locally. It’s an optimization so the server doesn’t have to compute a diff at all after event, example here: shopper_umbrella/apps/shopper_web/lib/shopper_web/channels/app_channel.ex at main · hubertlepicki/shopper_umbrella · GitHub

  • I keep versioning server state and client state, automatically. Whenever server changes state, it increments the version counter by 2, whenever client changes the state it does the same. By comparing the server and client version numbers, a component can see if the server is yet to react to some of the events the client sent out, or if the state it sees is up to date and won’t change. This is helpful for building “controlled inputs”, where we expect the server to transform the input of user and send back, or to skip some renders if we know the responses coming from server are not final just yet and there will be more coming shortly

There are some things I plan to experiment with too:

  • initialize server state in Phoenix controller request, and make it cached for a few seconds, so that when Phoenix channel connects we can skip the state initialization. This will probably change where I keep state, and I won’t keep it in the channel directly but rather in a separate process.

  • validation that the state being set is JSON-serializable, currently it just crashes. I think this extra validation will run in development & test, maybe it’s unnecessary in production. Need to ponder about this.

  • Phoenix presence support (React component)

  • I want to be able to generate a unique “page token” or “tab token”, something like this, that would be shared between all processes on the current page the user sees so that server-side communication between state channels is possible. Basically an automatic pubsub for all channels currently open in the page user sees at given time.

  • graceful shutdown for channels

2 Likes

I just merged a PR from a co-worker with my details in the testbed README. Let me know if that helps.

Hmm… I see, so the user can have more fine grained control over the patch generation if they want it. Makes sense. I notice in your code example, you are using patch_state in all cases. I assume from looking at the StateChannel code it’s also possible to just return a new state and let the diff get computed for you? I’m assuming this to be the ‘normal’ case. In general, I like to optimize for library consumer code simplicity.

Yeah, this seems like a really good idea. I’ll likely try to implement this also.

Yeah, I can definitely see the value of something like this. I currently have the client side library be able to share a LiveState connection via a provider and consumer idea, something like React Contexts but not tied to React. It would be even better if I didn’t have to do this, and could have the server just figure out that multiple components were part of the same logical connection and share state appropriately.

You may also be interested in Hologram which transpiles Elixir to JS and keeps the state on the client (which solves the problems described). Take a look at this thread: Hologram - full stack isomorphic Elixir web framework I plan to speed up development the beginning of the year, a demo website with docs should be released q1 23, and the Elixir standard lib should be transpiled in the beginning of q2 23.

2 Likes

@superchris

What drove you to authoring LiveState? Trying to get an impression of your vision on web development and the possible future directions of LiveState.

My question comes from a place of interest. Like what I see on the GitHub repo and have been working with web components more lately.

The original impetus for creating LiveState was that I wanted to see if I could use LiveView to build an embedded or third party app: something you could drop into a web page hosted somewhere else, like a comments section or shopping cart. My first attempt was to see if I could render a LiveView into a custom element, but this proved difficult technically and less appealing the deeper I got.

My next thought was: What if I did something like LiveView, but kept only the rendering on the client. This is what led me to LiveState. I realized that the core of LiveView is the pattern I named (for lack of an existing name) the Event State Reducer pattern: functions which take an event, the current state, and return a new state. The idea of LiveState is to implement this same pattern but keep rendering client side: I push events up over the channel, and push state back down. It started as an experiment to “see how it felt” to build applications this way. My experiences and the feedback from others seemed good, so I kept going, and here we are :slight_smile:

As far as a “vision on web development” that is a big question, probably too much to fit a comment reply :slight_smile: I’m very interested in continuing to explore the embedded app problem space, but whether the LiveState approach is a good approach to web app development more generally is an interesting question. As always, the answer is almost certainly: it depends.

I’ve been a proponent of web components for many years, so my intention is to support custom elements in a “first class” way while making it as easy as possible to support other frameworks. My philosophy, is as much as I have one, is to use the capabilities provided by the browser as much as I am able and avoid anything that locks me into a specific framework. I continue to be amazed by how much I see frameworks invent their own solutions to problems already solved by web standard APIs, but that is another rant for another day :slight_smile:

8 Likes

For me, inertiajs is about being able to swap out front end and backend frameworks with a common protocol.

I want to throw my project into the arena here as well. Confusingly it’s also called LiveData, but uses a quite different approach from a lot of other projects I have seen.

LiveData has a concept of tracked functions, defined with deft instead of a regular def. When compiling deft functions, they are transformed in a way which extracts static chunks of the data structure and sends them only once. Instead of a general tree diff, it draws inspiration from virtual dom frontend frameworks and enable you to specify keys/IDs for subtrees which enables the runtime to efficiently generate diffs for you.

  deft render(assigns) do
    %{
      posts: for post <- assigns.posts do
        keyed post.id do
          %{
            id: post.id,
            title: post.title,
            content: post.content,
          }
        end
      end
    }
  end

This is how the render function could look like in a LiveData. Other than this, things look pretty much exactly like a LiveView.

Here, keyed is used to specify an a key that can uniquely identify the post. By doing this you get the following advantages:

  • If a post changes, only the change in that post is ever sent.
  • No static content is sent twice, the posts key, content key + more in the map is static, so is only sent once at the beginning of the connection.
  • If a post is added, only the added post is sent.

It’s not very cleaned up, but here is a simple example of a LiveView: phoenix_data_view_demo/hello_world_data.ex at main · hansihe/phoenix_data_view_demo · GitHub

6 Likes

What can I say, great minds think alike :rofl:

But it’s both validating and encouraging that this is not just me N=1 thinking about this. Also, many different projects may result in fragmentation with no clear choice for users, temporarily, but we probably don’t need a dozen of different solution but 2 or 3 slightly different ones.

1 Like

this approach gets some more publicity:

1 Like

@superchris I’m having a play around with your library live_state and am really liking it. I’m using it with lit and your decorators. Whats your approach to handling errors? I’m thinking of 2 different types of errors; expected errors like changeset errors and unexpected errors like if you have a case statement with a catch all.

Great question! For actual errors (socket or channel failures for example) I’m underway with adding support for an error event from LiveState and added an issue to track it here: Error handling · Issue #5 · launchscout/live_state · GitHub. I have an initial implementation, but am not entirely happy with what I came up with and want to rework it a little.

For changeset “errors” (really validation failures) my thought is those belong on state. I put together a CRUD example recently to show how this could work, I need to figure out where to put it. With the amount of subprojects, examples and related things it might be time for a livestate github org. For now, I created an issue so I can remember to do this: CRUD example · Issue #6 · launchscout/live_state · GitHub

2 Likes

Thrilled about this…
I have started to look in to Sveltekit (1.0 now) with a Phoenix backend. This could the bridge between this great programming tools.
I there a NPM package for the client side of Svelte? I can find your examples but it seems you include packages that are local on your server? I am not up to speed on Svelte so might miss something.
Anyway, Phoenix/LiveData/Sveltekit (with Skeleton UI) looks like a really nice way forward for me.