ActiveState allows you to update your Svelte application state easily and in real-time from your Rails backend. It's a combination of an npm package and a Ruby gem that gives you an application-wide Svelte 5 $state
object and methods to manipulate this state object using dot-notation.
With ActiveState, you have an application-wide state object like this:
<script>
import { State } from 'activestate'
</script>
<h1>{State.projects[projectId].name}</h1>
And you can update it in real-time from your Rails backend like this:
ProjectChannel[project].state("projects", project.id, "name").set("My awesome project")
Let's say you have a web app and want to display real-time messages to a specific user. This can be easily done with ActiveState by pushing new messages to a centralized state object as they arrive. Let's take a look:
First, we need a channel to subscribe to:
# user_channel.rb
class UserChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
Then, inside your component, set up a subscription and iterate over State.messages
:
<script>
import { subscribe, State } from 'activestate'
import { onDestroy } from 'svelte'
// Set up a subscription to the UserChannel
const unsubscribe = subscribe('UserChannel', {user_id: something})
// Don't forget to unsubscribe when the component is destroyed
onDestroy(unsubscribe)
// Initialize it with an empty array
State.messages ||= []
const messages = $derived(State.messages)
</script>
{#each messages as message}
<p>{message.text}</p>
{/each}
Now you can push directly into State.messages
from your server through the UserChannel:
# Somewhere in your Ruby code:
UserChannel[some_user].state('messages').push({id: 4, text: "Hello from Ruby"})
And you can update the message:
UserChannel[some_user].state('messages').upsert({id: 4, text: "Changed text"})
All state modifications operate on a path. This path can be specified using dot-notation (e.g., projects.421.name
) or as an array (e.g., ["projects", project.id, "name"]
).
ActiveState comes with 5 built-in modifiers: set
, assign
, push
, upsert
, and delete
:
set(data)
UserChannel[some_user].state('current_user.name').set("John")
Replaces the value of current_user.name
with John
.
assign(data)
UserChannel[some_user].state('current_user').assign({name: 'new name'})
Uses Object.assign
to merge the passed object onto current_user
.
push(data)
Pushes data onto the array. If the specified path doesn't exist, it will be initialized as a new array.
upsert(data, key = "id")
UserChannel[some_user].state('current_user.notices').upsert([{id: 4, name: "new name"}])
This iterates over the array in current_user.notices
and performs an upsert using the specified key.
delete({key: val})
If called on an array, it iterates over the array and deletes all entries whose keys match the provided object.
delete(key)
If called on an object, it deletes the provided key from the object.
You can also call functions that natively exist on objects in the state. For example, if you have an array in current_user.notices
, you can call its native push
function:
UserChannel[some_user].state('current_user.notices').push "next chunk"
You can also define custom methods to modify your state:
import { registerMutator, State } from 'activestate'
registerMutator('append', function(currentValue, data) {
return currentValue.concat(data)
})
<p>
Here is a very long string: {State.long_string}
</p>
UserChannel[some_user].state('long_string').append "next chunk"
The state
method is also available on a Channel instance. This means you can update Svelte stores through a specific connection instead of broadcasting to all subscribers:
# user_channel.rb
class UserChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
state('current_user').set(current_user.as_json)
end
end
When using ActiveState in a Server-Side Rendering (SSR) context, the server-side JavaScript process is usually shared between requests. Therefore, it's important to call reset()
before or after rendering to clear the state and avoid data leakage between requests:
import { reset } from 'activestate'
reset()
// ... rest of code comes here
I wasn't sure at first, but now I think it's awesome. Svelte 5 introduced fine-grained reactivity for objects declared with $state
. This means that even if your object becomes huge with deeply nested data, it should not impact performance. So yes, I think it's clever and smart! 🤓
Add this line to your application's Gemfile:
gem 'activestate'
And then execute:
$ bundle install
Install the package:
$ npm i -D activestate