hotwired / stimulus

A modest JavaScript framework for the HTML you already have
https://stimulus.hotwired.dev/
MIT License
12.67k stars 421 forks source link

Discussion/Questions: Hotwire and Client Side Rendering #770

Closed MSILycanthropy closed 4 months ago

MSILycanthropy commented 4 months ago

Stimulus works wonderfully when modifying existing markup, but I've noticed some friction in adding new markup to a page in certain cases, namely when the state only exists on the client. Consider the example of a shopping cart that shows the items I've selected. Selecting an item doesn't update anything on the server side.

From what I can tell there isn't a great out of the box solution for this in Hotwire currently. Turbo works really well when we have to hit the server to do something. We can replace a frame, render a stream, and update the DOM in whatever way we want. But, if there isn't an action to do on the server it seems like Hotwire has no answer for how to render things in a scenario like this.

For simple cases, hiding elements and modifying attributes with Stimulus work quite well. But when the UI is sufficiently complex and on the client, it starts to fall apart.

This use case feels tangentially related to the things Stimulus is really good at, but also potentially outside it's wheelhouse.

Here's how I've been handling it,

Solution I've been using

In cases like this what I've begun doing is using something like @github/jtml or lit-html. The main benefit being automatic escaping, intelligent DOM updates, and just overall ease of use compared to alternatives like innerHTML.

Here's how I might implement that checkout example with it,

import { render, html } from "@github/jtml"

/* stimulus things */

// here items is an array of objects
selectedItemsValueChanged(items) {
  const content = html`
    ${items.map((item) => html`
      <div>
        Item: ${item.name}
        Cost: ${item.amount}
      </div>
    `)}
  `

  render(content, this.cartTarget)
}

The best part about this is it keeps us in Stimulus, I don't need to reach for React/Svelte/Vue when things get a little complex on the client. Which they inevitably will.

A Few Questions

Ultimately Stimulus is meant to be the framework for "HTML you already have", so are things like this outside what Stimulus should be used for? Is there a more "Hotwire" way to implement something like the above?

Depending on the answer to that question, does it make sense to provide documentation on the use case? Or potentially add features related to it?

With my current understanding of things, this feels like an odd gap in the ecosystem that not many people are talking about. Curious what others think!

tpaulshippy commented 4 months ago

Wouldn't you want the selected items in a shopping cart to be held on the server?

  1. So that the user still has them after they close the window.
  2. In order to do data analysis of what people are shopping for.
  3. So that the user can switch devices (assuming they have an account) and maintain their cart. ...other reasons...

I suppose you could accomplish #1 with cookies alone but that seems messy.

MSILycanthropy commented 4 months ago

@tpaulshippy ultimately with a cart yeah you probably would, not a great example. But, that's beside the point.

There are still ultimately UXs that exist that don't require hitting the server. What is the suggested way to handle those with Hotwire?

Maybe that's a flawed question in itself?

tpaulshippy commented 4 months ago

It's a good question. But it does sorta go to the heart of the Hotwire philosophy.

There are still ultimately UXs that exist that don't require hitting the server.

Are there? Doesn't every UX ultimately come from the server at some point? You said "hitting the server" which implies a UX that has already been loaded to the client. Sure, and I think anyone would admit that Hotwire is not as good at building UX that is 100% client side (many kinds of games, for example). But you might be surprised at how powerful a UX can be built by delivering HTML from the server as the primary means of showing interfaces. That is part of the argument the makers of libraries and frameworks like Hotwire, Phoenix Liveview, and htmx are making.

If you have another specific scenario in mind I'd love to hear more about it.

MSILycanthropy commented 4 months ago

Yeah, ultimately every UX at least starts at the server. And mostly almost immediately need to go back to do something there again. Which is why Hotwire/LiveView/htmx/etc work so well. It's super awesome, and incredibly powerful. Not doubting that at all, been using it for quite awhile now and it's really revolutionized some of the things I work on. Just sort of ran into a weird.. middle scenario, that I'm not quite sure how to handle with Hotwire?

I'll shed a bit of context on what even got me thinking about this. But basically, it's just a complex form. Data isn't yet persisted until saved, but there is dependent state on that data that is yet to be persisted.

The product I work on is a CRM software. Currently, our users have their clients contact info in our system, and those clients can be grouped into families. But for various legacy reasons, things are a bit messy. A given family might have all the right contact info in there, but it's spread out across all the people. Maybe mom's phone number is on all 3 kids, and mom doesn't exist in the system, things like that.

So, we're providing a way to remedy that. Effectively, this form let's you reassign contact info, add new contact info, and determine who the primary point of contact is for that family.

Originally I built it with Turbo, and my solution was to use multiple submit buttons to handle it, with varying formactions. For example, there's a button to add a phone number, it streams down the new fields and uses the data from the form to keep clients state correct. Like, who can be assigned a phone number, since we only allow one per person. Which felt.. weird to me? Could totally be the completely wrong approach.

The Turbo solution worked. It just felt.. kinda odd? Ends up being a lot of extra requests for a bunch of state that exists only on the client until the point of submission. Ultimately it felt like bending Turbo to make it work, not something that Turbo was intended to be used for.

The key part is, most of it works super nicely with Turbo. But at that boundary where there's unpersisted dependent state on the client, I've got a wee bit of CSR like shown above. Just to kinda tie the two boundaries together. Which is what prompted this, it felt odd to me that there wasn't a solution built in.

Ultimately, I could be totally missing the mark here still. Please let me know if I am! Thanks a ton.

TLDR: Complex forms that have dependent state on unpersisted data is the heart of it.

MSILycanthropy commented 4 months ago

Okay so, after having voiced those thoughts, I think I've actually talked myself through everything.

Ultimately, I think the solution is just.. don't pass that state back to the server. Any changes that need to take place can just happen on the client after the server renders the markup.

That seems like a much happier path, and I think should suffice?