Real-Time Rails Without Turbo: Modern Reactive UIs with Inertia and DexieCable
DEV Community

Real-Time Rails Without Turbo: Modern Reactive UIs with Inertia and DexieCable

The Problem: Real-Time in Inertia Apps

Inertia.js gives us monolith productivity with the rich UI component model of Svelte, Vue, or React. However, handling real-time updates over WebSockets in an Inertia app typically leads to one of two awkward patterns:

  • Inertia Reloads (router.reload({ only: ['todos'] })): Every WebSocket event triggers a network request back to Rails to fetch updated props. It works, but it causes unnecessary server load and adds latency to UI updates.
  • Manual Component State Sync: You listen to ActionCable events and manually splice arrays or update objects inside frontend state stores. This scales poorly and quickly leads to brittle client-side logic.

The Solution: Local-First Synchronization with DexieCable

DexieCable bridges the gap between Rails (via ActionCable) and client-side IndexedDB (via Dexie.js). Instead of pushing WebSocket updates directly into your UI components, DexieCable allows Rails to execute Dexie.js write operations straight into IndexedDB over ActionCable. Your UI components then subscribe to Dexie using reactive liveQuery.

[ Rails / ActionCable ] | [ DexieCable ] | [ Dexie (IndexedDB) ] | [ LiveQuery ]

This architecture gives you some significant benefits:

  • Zero Sync Boilerplate: Components don't care how data arrived in IndexedDB; they simply observe local tables.
  • Instant UI Updates: UI rendering runs off browser memory/IndexedDB-eliminating network render delays.
  • Decoupled Architecture: ActionCable updates IndexedDB in the background, regardless of which page or component is currently mounted.
  • Declarative Rails Macros: You can automate model broadcasting on the backend using simple ActiveRecord macros.

How It Works in Practice

Let’s look at a complete example using Rails, ActionCable, DexieCable, and Svelte.

1. Setting Up the Client (Dexie + DexieCable)

Configure your Dexie database and point DexieCable to your database instance.

// db.js
import Dexie from "dexie";
import DexieCable from "dexiecable";

export const db = new Dexie("MyAppDB");
db.version(1).stores({
  todos: "id, title, completed, updated_at"
});

// Pass your Dexie database instance to DexieCable and subscribe to your channel
DexieCable.db = db;
DexieCable.subscribe("UserChannel");

2. Setting Up Rails (Channel & Model)

First, include DexieCable in your ActionCable channel:

# app/channels/user_channel.rb
class UserChannel < ApplicationCable::Channel
  include DexieCable

  def subscribed
    stream_for current_user
  end
end

Next, use the syncs_to_dexie macro on your model:

# app/models/todo.rb
class Todo < ApplicationRecord
  belongs_to :user

  # Automatically syncs create (add), update (put), and destroy (delete) events
  syncs_to_dexie via: UserChannel, to: :user
end

With syncs_to_dexie, whenever a Todo is created, updated, or deleted, DexieCable automatically broadcasts the corresponding Dexie operation targeting the user's channel stream.

3. Reactive Rendering in Svelte

In your Svelte + Inertia view, query Dexie using liveQuery. When ActionCable pushes changes, Dexie updates IndexedDB, and Svelte reactively re-renders the UI automatically.

<!-- Todos.svelte -->
<script>
  import { db } from './db';
  import { liveQuery } from 'dexie';

  // Observe the local IndexedDB table reactively
  let todos = liveQuery(() => db.todos.toArray());
</script>

<div class="todo-list">
  <h1>Real-Time Todos</h1>

  {#if $todos}
    <ul>
      {#each $todos as todo (todo.id)}
        <li class:completed={todo.completed}>
          {todo.title}
        </li>
      {/each}
    </ul>
  {/if}
</div>

Advanced Query Chaining from Rails

syncs_to_dexie covers standard CRUD synchronization, but DexieCable also lets you chain arbitrary Dexie operations directly from Rails controllers or background jobs:

# Single item insert
UserChannel[current_user].table("todos").add(id: 1, title: "Buy milk") #[cite: 1]

# Modify matching records
UserChannel[current_user]
  .table("todos")
  .where(:completed).equals(false)
  .modify(completed: true) #[cite: 1]

# Delete specific scopes
UserChannel[current_user]
  .table("todos")
  .where(:project_id).equals(project.id)
  .delete() #[cite: 1]

DexieCable serializes the method chain into JSON, sends it across ActionCable, and replays the exact operation chain against the local IndexedDB database in the browser.

Why Choose This Over Turbo Streams?

Turbo Streams couple your backend directly to HTML fragment generation or DOM manipulation. By choosing the DexieCable + Inertia + Svelte approach:

  • You keep complete control over your frontend state inside Svelte components.
  • Your backend serves pure data rather than rendering HTML partials over WebSockets.
  • Your UI feels immediate because reads occur locally against IndexedDB.

Wrapping Up

If you prefer the Rails + Inertia stack but want reactive real-time updates without Turbo, DexieCable provides a lightweight pattern that bridges the gap. Rails manages data and business logic, ActionCable handles transport, Dexie manages local browser storage, and Svelte delivers the UI.

Check out the repository on GitHub:
πŸ‘‰ github.com/buhrmi/dexiecable

Top comments (0)

Comments

No comments yet. Start the discussion.