Skip to content

Instantly share code, notes, and snippets.

@hopsoft
Last active March 23, 2025 01:59
Show Gist options
  • Save hopsoft/22170e3c792ec8d9279ae49eb67236dc to your computer and use it in GitHub Desktop.
Save hopsoft/22170e3c792ec8d9279ae49eb67236dc to your computer and use it in GitHub Desktop.
TurboBoost State

TurboBoost

Note

The new TurboBoost monorepo, including the State feature is still WIP. Most of the functionality has been developed and tested. Unfortunately, the effort is currently on pause as I'm tackling other priorities right now.

State

TurboBoost manages various forms of state to provide a terrific reactive user experience.

Here’s a breakdown of each type:

Server-State

Server-State is the persistent state that the server used for the most recent render. This state is signed, ensuring data integrity and security.

The client includes this signed state along with its own optimistic changes whenever a Command is invoked. The server can then compute the difference between the Client-State and the Server-State, allowing you to accept or reject the client's optimistic changes.

This ensures the server remains the single source of truth.

Server-State can be accessed within Commands like so.

state[:key] = "value"
state[:key]
#=> "value"

Server-State is also accessible in controllers and views.

# controller
turbo_boost.state[:key] = "value"
turbo_boost.state[:key]
#=> "value"
<%
  # view
  turbo_boost.state[:key] = "value"
  turbo_boost.state[:key]
  #=> "value"
%>

Now-State

Now-State is ephemeral server side state that only exists for the current render cycle. Similar to flash.now in Rails, this state is discarded after rendering.

It’s useful for managing temporary data that doesn’t need to persist beyond the current request.

Now-State can be accessed within Commands like so.

state.now[:key] = "value"
state.now[:key]
#=> "value"

Now-State is also accessible in controllers and views.

# controller
turbo_boost.state.now[:key] = "value"
turbo_boost.state.now[:key]
#=> "value"
<%
  # view
  turbo_boost.state.now[:key] = "value"
  turbo_boost.state.now[:key]
  #=> "value"
%>

Client-State

Client-State is a mutable version of the signed Server-State, wrapped in an observable JavaScript proxy. This allows for sophisticated techniques like data binding via custom JavaScript, Stimulus controllers, or web components.

Client-State enables immediate UI updates, providing a fast and smooth user experience while the server resolves state differences whenever Commands are invoked.

Client-State can be accessed on the client like so.

TurboBoost.State.current['key'] = 'value'
TurboBoost.State.current['key']
//=> 'value'

Page-State

Page-State is managed by the client and used to remember element attribute values between server renders. It’s best for tracking transient user interactions, such as - which elements are visible, open/closed, their position, etc.

This enhances the user experience by maintaining the state of UI elements between renders. When invoking commands, the client sends the Page-State to the server, allowing it to preserve element attributes when rendering. The client also checks and restores Page-State whenever the DOM changes if needed.

You can opt-in to remember Page-State with Rails tag helpers via the turbo_boost[:remember] option.

<%= tag.details id: "page-state-example", open: "open", turbo_boost: { remember: [:open] } do %>
  <summary>Page-State Example</summary>
  Content...
<% end %>

This will remember whether the details element is open or closed.

That's it! You're done.

Note

Page-State tracking works with all element attributes, including aria, data, and even custom attributes. Elements must have a unique id to participate in Page-State tracking.

State Resolution

Commands can perform state resolution by implementing the resolve_state method.

The Command has access to all forms of state, so you should use explicit access during resolution.

You can access both the signed Server-State and the optimistc Client-State from within the Command like so.

class ExampleCommand < TurboBoost::Commands::Command

  def resolve_state
    state.signed #=> the Server-State (from the last render)
    state.unsigned #=> the optimistic Client-State
    # compare and resolve the delta
  end
end

Tip

State resolution can involve data lookups, updates to persistent data stores, calls to 3rd party APIs, etc.

You can opt-in to state resolution with the following config option.

# config/initializers/turbo_boost.rb
TurboBoost::Commands.config.tap do |config|
  config.resolve_state = true
end

Tip

TurboBoost State mechanics can also be used independent of Commands with standard Hotwire techniques.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment