Rocket

Rocket is currently in alpha – available with Datastar Pro.

Rocket is a Datastar Pro plugin that bridges Web Components with Datastar’s reactive system. It allows you to create encapsulated, reusable components with reactive data binding.

Rocket is a powerful feature, and should be used sparingly. For most applications, standard Datastar templates and global signals are sufficient. Reserve Rocket for cases where component encapsulation is essential, such as integrating third-party libraries or creating complex, reusable UI elements.

Basic example #

Traditional web components require verbose class definitions and manual DOM management. Rocket eliminates this complexity with a declarative, template-based approach.

Here’s a Rocket component compared to a vanilla web component.

 1<template data-rocket:simple-counter
 2          data-props:count="int|min:0|=0"
 3          data-props:start="int|min:0|=0"
 4          data-props:step="int|min:1|max:10|=1"
 5>
 6  <script>
 7    $$count = $$start
 8  </script>
 9  <div data-if="$$errs?.start" data-text="$$errs.start[0].value"></div>
10  <div data-if="$$errs?.step" data-text="$$errs.step[0].value"></div>
11  <button data-on:click="$$count -= $$step">-</button>
12  <span data-text="$$count"></span>
13  <button data-on:click="$$count += $$step">+</button>
14  <button data-on:click="$$count = $$start">Reset</button>
15</template>
  1class SimpleCounter extends HTMLElement {
  2  static observedAttributes = ['start', 'step'];
  3  
  4  constructor() {
  5    super();
  6    this.innerHTML = `
  7      <div class="error" style="display: none;"></div>
  8      <button class="dec">-</button>
  9      <span class="count">0</span>
 10      <button class="inc">+</button>
 11      <button class="reset">Reset</button>
 12    `;
 13    
 14    this.errorEl = this.querySelector('.error');
 15    this.decBtn = this.querySelector('.dec');
 16    this.incBtn = this.querySelector('.inc');
 17    this.resetBtn = this.querySelector('.reset');
 18    this.countEl = this.querySelector('.count');
 19    
 20    this.handleDec = () => { 
 21      const newValue = this.count - this.step;
 22      if (newValue >= 0) {
 23        this.count = newValue;
 24        this.updateDisplay();
 25      }
 26    };
 27    this.handleInc = () => { 
 28      this.count += this.step;
 29      this.updateDisplay();
 30    };
 31    this.handleReset = () => { 
 32      this.count = this.start; 
 33      this.updateDisplay(); 
 34    };
 35    
 36    this.decBtn.addEventListener('click', this.handleDec);
 37    this.incBtn.addEventListener('click', this.handleInc);
 38    this.resetBtn.addEventListener('click', this.handleReset);
 39  }
 40  
 41  connectedCallback() {
 42    const startVal = parseInt(this.getAttribute('start') || '0');
 43    const stepVal = parseInt(this.getAttribute('step') || '1');
 44    
 45    if (startVal < 0) {
 46      this.errorEl.textContent = 'start must be at least 0';
 47      this.errorEl.style.display = 'block';
 48      this.start = 0;
 49    } else {
 50      this.start = startVal;
 51      this.errorEl.style.display = 'none';
 52    }
 53    
 54    if (stepVal < 1 || stepVal > 10) {
 55      this.errorEl.textContent = 'step must be between 1 and 10';
 56      this.errorEl.style.display = 'block';
 57      this.step = Math.max(1, Math.min(10, stepVal));
 58    } else {
 59      this.step = stepVal;
 60      if (this.start === startVal) {
 61        this.errorEl.style.display = 'none';
 62      }
 63    }
 64    
 65    this.count = this.start;
 66    this.updateDisplay();
 67  }
 68  
 69  disconnectedCallback() {
 70    this.decBtn.removeEventListener('click', this.handleDec);
 71    this.incBtn.removeEventListener('click', this.handleInc);
 72    this.resetBtn.removeEventListener('click', this.handleReset);
 73  }
 74  
 75  attributeChangedCallback(name, oldValue, newValue) {
 76    if (name === 'start') {
 77      const startVal = parseInt(newValue || '0');
 78      if (startVal < 0) {
 79        this.errorEl.textContent = 'start must be at least 0';
 80        this.errorEl.style.display = 'block';
 81        this.start = 0;
 82      } else {
 83        this.start = startVal;
 84        this.errorEl.style.display = 'none';
 85      }
 86      this.count = this.start;
 87    } else if (name === 'step') {
 88      const stepVal = parseInt(newValue || '1');
 89      if (stepVal < 1 || stepVal > 10) {
 90        this.errorEl.textContent = 'step must be between 1 and 10';
 91        this.errorEl.style.display = 'block';
 92        this.step = Math.max(1, Math.min(10, stepVal));
 93      } else {
 94        this.step = stepVal;
 95        this.errorEl.style.display = 'none';
 96      }
 97    }
 98    if (this.isConnected) {
 99      this.updateDisplay();
100    }
101  }
102  
103  updateDisplay() {
104    this.countEl.textContent = this.count;
105  }
106}
107
108customElements.define('simple-counter', SimpleCounter);

Overview #

Rocket allows you to turn HTML templates into fully reactive web components. The backend remains the source of truth, but your frontend components are now encapsulated and reusable without any of the usual hassle.

Add data-rocket:my-component to a template element to turn it into a Rocket component. Component signals are automatically scoped with $$, so component instances don’t interfere with each other.

You can use Rocket to wrap external libraries using module imports, and create references to elements within your component. Each component gets its own signal namespace that plays nicely with Datastar’s global signals. When you remove a component from the DOM, all its $$ signals are cleaned up automatically.

Bridging Web Components and Datastar #

Web components want encapsulation; Datastar wants a global signal store. Rocket gives you both by creating isolated namespaces for each component. Each instance gets its own sandbox that doesn’t mess with other components on the page, or with global signals.

Multiple component instances work seamlessly, each getting its own numbered namespace. You still have access to global signals when you need them, but your component state stays isolated and clean.

Signal Scoping #

Use $$ for component-scoped signals, and $ for global signals. Component signals are automatically cleaned up when you remove the component from the DOM - no memory leaks, no manual cleanup required.

Behind the scenes, your $$count becomes something like $._rocket.my_counter._1.count, with each instance getting its own numbered namespace. You never have to think about this complexity - just write $$count and Rocket handles the rest.

 1// Your component template writes:
 2<button data-on:click="$$count++">Increment</button>
 3<span data-text="$$count"></span>
 4
 5// Rocket transforms it to (for instance #1):
 6<button data-on:click="$._rocket.my_counter._1.count++">Increment</button>
 7<span data-text="$._rocket.my_counter._1.count"></span>
 8
 9// The global Datastar signal structure:
10$._rocket = {
11  my_counter: {
12    _1: { count: 0 }, // First counter instance
13    _2: { count: 5 }, // Second counter instance
14    _3: { count: 10 } // Third counter instance
15  },
16  user_card: {
17    _4: { name: "Alice" }, // Different component type
18    _5: { name: "Bob" }
19  }
20}

Defining Rocket Components #

Rocket components are defined using a HTML template element with the data-rocket:my-component attribute, where my-component is the name of the resulting web component. The name must contain at least one hyphen, as per the custom element specification.

1<template data-rocket:my-counter>
2  <script>
3    $$count = 0  
4  </script>
5  <button data-on:click="$$count++">
6    Count: <span data-text="$$count"></span>
7  </button>
8</template>

This gets compiled to a web component, meaning that usage is simply:

1<my-counter></my-counter>

Signal Management #

Rocket makes it possible to work with both component-scoped and global signals (global to the entire page).

Component Signals #

Component-scoped signals use the $$ prefix and are isolated to each component instance.

 1<template data-rocket:isolated-counter>
 2  <script>
 3    // These are component-scoped – each instance has its own values
 4    $$count = 0
 5    $$step = 1
 6    $$maxCount = 10
 7    $$isAtMax = computed(() => $$count >= $$maxCount)
 8    
 9    // Component actions
10    actions.increment = () => {
11      if ($$count < $$maxCount) {
12        $$count += $$step
13      }
14    }
15  </script>
16  
17  <div>
18    <p>Count: <span data-text="$$count"></span></p>
19    <p data-show="$$isAtMax" class="error">Maximum reached!</p>
20    <button data-on:click="@increment()" data-attr:disabled="$$isAtMax">+</button>
21  </div>
22</template>
23
24<!-- Multiple instances work independently -->
25<isolated-counter></isolated-counter>
26<isolated-counter></isolated-counter>

Global Signals #

Global signals use the $ prefix and are shared across the entire page.

 1<template data-rocket:theme-toggle>
 2  <script>
 3    // Access global theme state
 4    if (!$theme) {
 5      $theme = 'light'
 6    }
 7    
 8    actions.toggleTheme = () => {
 9      $theme = $theme === 'light' ? 'dark' : 'light'
10    }
11  </script>
12  
13  <button data-on:click="@toggleTheme()">
14    <span data-text="$theme === 'light' ? '🌙' : '☀️'"></span>
15    <span data-text="$theme === 'light' ? 'Dark Mode' : 'Light Mode'"></span>
16  </button>
17</template>
18
19<!-- All instances share the same global theme -->
20<theme-toggle></theme-toggle>
21<theme-toggle></theme-toggle>

Props #

The data-props:* attribute allows you to define component props with codecs for validation and defaults.

 1<!-- Component definition with defaults -->
 2<template data-rocket:progress-bar
 3          data-props:value="int|=0"
 4          data-props:max="int|=100" 
 5          data-props:color="string|=blue"
 6>
 7  <script>
 8    $$percentage = computed(() => Math.round(($$value / $$max) * 100))
 9  </script>
10  
11  <div class="progress-container">
12    <div class="progress-bar" 
13        data-style="{
14          width: $$percentage + '%',
15          backgroundColor: $$color
16        }">
17    </div>
18    <span data-text="$$percentage + '%'"></span>
19  </div>
20</template>
21
22<!-- Usage -->
23<progress-bar data-attr:value="'75'" data-attr:color="'green'"></progress-bar>
24<progress-bar data-attr:value="'30'" data-attr:max="'50'"></progress-bar>

Rocket automatically transforms and validates values using the codecs defined in data-props:* attributes.

Setup Scripts #

Setup scripts initialize component behavior and run when the component is created. Rocket supports both component (per-instance) and static (one-time) setup scripts.

Component Setup Scripts #

Regular <script> tags run for each component instance.

 1<template data-rocket:timer
 2          data-props:seconds="int|=0"
 3          data-props:running="boolean|=false"
 4          data-props:interval="int|=1000"
 5>
 6  <script>
 7    $$minutes = computed(() => Math.floor($$seconds / 60))
 8    $$displayTime = computed(() => {
 9      const m = String($$minutes).padStart(2, '0')
10      const s = String($$seconds % 60).padStart(2, '0')
11      return m + ':' + s
12    })
13    
14    let intervalId
15    effect(() => {
16      if ($$running) {
17        intervalId = setInterval(() => $$seconds++, $$interval)
18      } else {
19        clearInterval(intervalId)
20      }
21    })
22    
23    // Cleanup when component is removed
24    onCleanup(() => {
25      clearInterval(intervalId)
26    })
27  </script>
28  
29  <div>
30    <h2 data-text="$$displayTime"></h2>
31    <button data-on:click="$$running = !$$running" 
32            data-text="$$running ? 'Stop' : 'Start'">
33    </button>
34    <button data-on:click="$$seconds = 0">Reset</button>
35</div>
36</template>

Static Setup Scripts #

Scripts with a data-static attribute only run once, when the component type is first registered. This is useful for shared constants or utilities.

 1<template data-rocket:icon-button>
 2  <script data-static>
 3    const icons = {
 4      heart: '❤️',
 5      star: '⭐',
 6      thumbs: '👍',
 7      fire: '🔥'
 8    }
 9  </script>
10  
11  <script>
12    $$icon = $$type || 'heart'
13    $$emoji = computed(() => icons[$$icon] || '❓')
14  </script>
15  
16  <button data-on:click="@click()">
17    <span data-text="$$emoji"></span>
18    <span data-text="$$label || 'Click me'"></span>
19  </button>
20</template>

Module Imports #

Rocket allows you to wrap external libraries, loading them before the component initializes and the setup script runs. Use data-import-esm:* for modern ES modules and data-import-iife:* for legacy global libraries.

ESM Imports #

The data-import-esm:* attribute should be used for modern ES modules.

 1<template data-rocket:qr-generator
 2          data-props:text="string|trim|required!|=Hello World"
 3          data-props:size="int|min:50|max:1000|=200"
 4          data-import:esm-qr="https://cdn.jsdelivr.net/npm/[email protected]/+esm"
 5>
 6  <script>
 7    $$errorText = ''
 8    
 9    effect(() => {
10      // Check for validation errors first
11      if ($$hasErrs) {
12        const messages = []
13        if ($$errs?.text) {
14          messages.push('Text is required')
15        }
16        if ($$errs?.size) {
17          messages.push('Size must be 50-1000px')
18        }
19        $$errorText = messages.join(', ') || 'Validation failed'
20        return
21      }
22
23      if (!$$canvas) {
24        return
25      }
26
27      if (!qr) {
28        $$errorText = 'QR library not loaded'
29        return
30      }
31      
32      try {
33        qr.render({
34          text: $$text,
35          size: $$size
36        }, $$canvas)
37        $$errorText = ''
38      } catch (err) {
39        $$errorText = 'QR generation failed'
40      }
41    })
42  </script>
43  
44  <div data-style="{width: $$size + 'px', height: $$size + 'px'}">
45    <canvas data-if="!$$errorText" data-ref="canvas" style="display: block;"></canvas>
46    <div data-else data-text="$$errorText" class="error"></div>
47  </div>
48</template>

IIFE Imports #

The data-import-iife:* attribute should be used for legacy libraries. The library must expose a global variable that matches the alias you specify after data-import-iife:.

 1<template data-rocket:chart
 2          data-props:data="json|=[]"
 3          data-props:type="string|=line"
 4          data-import-iife:chart="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js"
 5>
 6  <script>
 7    let chartInstance
 8    
 9    effect(() => {
10      if (!$$canvas || !chart || !$$data.length) {
11        return
12      }
13
14      if (chartInstance) {
15        chartInstance.destroy()
16      }
17      
18      const ctx = $$canvas.getContext('2d')
19      chartInstance = new chart.Chart(ctx, {
20        type: $$type,
21        data: {
22          datasets: [{
23            data: $$data,
24            backgroundColor: '#3b82f6'
25          }]
26        }
27      })
28    })
29    
30    onCleanup(() => {
31      if (chartInstance) {
32        chartInstance.destroy()
33      }
34    })
35  </script>
36  
37  <canvas data-ref="canvas"></canvas>
38</template>

Rocket Attributes #

In addition to the Rocket-specific data-* attributes defined above, the following attributes are available within Rocket components.

data-if #

Conditionally outputs an element based on an expression.

1<div data-if="$$items.count" 
2     data-text="$$items.count + ' items'">
3></div>

data-else-if #

Conditionally outputs an element based on an expression, if the preceding data-if condition is falsy.

1<div data-if="$$items.count" 
2     data-text="$$items.count + ' items found.'">
3></div>
4<div data-else-if="$$items.count == 1"
5     data-text="$$items.count + ' item found.'">
6></div>

data-else #

Outputs an element if the preceding data-if and data-else-if conditions are falsy.

1<div data-if="$$items.count" 
2     data-text="$$items.count + ' items found.'">
3></div>
4<div data-else>No items found.</div>

data-for #

Loops over any iterable (arrays, maps, sets, strings, and plain objects), and outputs the element for each item.

1<div data-for="item, index in $$items">
2  <span data-text="index + ': ' + item.name"></span>
3</div>

The first alias (item above) is available to descendants just like any other binding. An optional second alias (index above) exposes the current key or numeric index. Nested loops are supported, and inner loop variables automatically shadow outer ones, so you can reuse names without conflicts.

1<div data-for="items in $$itemSet">
2  <div data-for="item in items">
3    <span data-text="item.name"></span>
4  </div>
5</div>

Reactive Patterns #

Rocket provides computed and effect functions for declarative reactivity. These keep your component state automatically in sync with the DOM.

Computed Values #

Computed values automatically update when their dependencies change.

 1<template data-rocket:shopping-cart
 2          data-props:items="json|=[]"
 3>
 4  <script>
 5    // Computed values automatically recalculate
 6    $$total = computed(() => 
 7      $$items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
 8    )
 9    
10    $$itemCount = computed(() =>
11      $$items.reduce((sum, item) => sum + item.quantity, 0)
12    )
13    
14    $$isEmpty = computed(() => $$items.length === 0)
15    
16    // Actions that modify reactive state
17    actions.addItem = (item) => {
18      $$items = [...$$items, { ...item, quantity: 1 }]
19    }
20    
21    actions.removeItem = (index) => {
22      $$items = $$items.filter((_, i) => i !== index)
23    }
24  </script>
25  
26  <div>
27    <h3>Shopping Cart</h3>
28    <p data-show="$$isEmpty">Cart is empty</p>
29    <p data-show="!$$isEmpty">
30      Items: <span data-text="$$itemCount"></span> | 
31      Total: $<span data-text="$$total.toFixed(2)"></span>
32    </p>
33    
34    <div data-for="item, index in $$items">
35      <span data-text="item.name"></span> - 
36      <span data-text="'$' + item.price"></span>
37      <button data-on:click="@removeItem(index)">Remove</button>
38    </div>
39  </div>
40</template>

Effects and Watchers #

Effects run side effects when reactive values change.

 1<template data-rocket:auto-saver
 2          data-props:data="string|="
 3          data-props:last-saved="string|="
 4          data-props:saving="boolean|=false"
 5>
 6  <script>
 7    let saveTimeout
 8    
 9    // Auto-save effect
10    effect(() => {
11      if (!$$data) {
12        return
13      }
14      
15      clearTimeout(saveTimeout)
16      saveTimeout = setTimeout(async () => {
17        $$saving = true
18        try {
19          await actions.post('/api/save', { data: $$data })
20          $$lastSaved = new Date().toLocaleTimeString()
21        } catch (error) {
22          console.error('Save failed:', error)
23        } finally {
24          $$saving = false
25        }
26      }, 1000) // Debounce by 1 second
27    })
28    
29    // Theme effect
30    effect(() => {
31      if ($theme) {
32        document.body.className = $theme + '-theme'
33      }
34    })
35    
36    onCleanup(() => {
37      clearTimeout(saveTimeout)
38    })
39  </script>
40  
41  <div>
42    <textarea data-bind="data" placeholder="Start typing..."></textarea>
43    <p data-show="$$saving">Saving...</p>
44    <p data-show="$$lastSaved">Last saved: <span data-text="$$lastSaved"></span></p>
45  </div>
46</template>

Element References #

You can use data-ref to create references to elements within your component. Element references are available as $$elementName signals and automatically updated when the DOM changes.

 1<template data-rocket:canvas-painter
 2          data-props:color="string|=#000000"
 3          data-props:brush-size="int|=5"
 4>
 5  <script>
 6    let ctx
 7    let isDrawing = false
 8    
 9    // Get canvas context when canvas is available
10    effect(() => {
11      if ($$canvas) {
12        ctx = $$canvas.getContext('2d')
13        ctx.strokeStyle = $$color
14        ctx.lineWidth = $$brushSize
15        ctx.lineCap = 'round'
16      }
17    })
18    
19    // Update drawing properties
20    effect(() => {
21      if (ctx) {
22        ctx.strokeStyle = $$color
23        ctx.lineWidth = $$brushSize
24      }
25    })
26    
27    actions.startDrawing = (e) => {
28      isDrawing = true
29      const rect = $$canvas.getBoundingClientRect()
30      ctx.beginPath()
31      ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top)
32    }
33    
34    actions.draw = (e) => {
35      if (!isDrawing) {
36        return
37      }
38
39      const rect = $$canvas.getBoundingClientRect()
40      ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top)
41      ctx.stroke()
42    }
43    
44    actions.stopDrawing = () => {
45      isDrawing = false
46    }
47    
48    actions.clear = () => {
49      if (ctx) {
50        ctx.clearRect(0, 0, $$canvas.width, $$canvas.height)
51      }
52    }
53  </script>
54  
55  <div>
56    <div>
57      <label>Color: <input type="color" data-bind="color"></label>
58      <label>Size: <input type="range" min="1" max="20" data-bind="brushSize"></label>
59      <button data-on:click="@clear()">Clear</button>
60    </div>
61    
62    <canvas 
63      data-ref="canvas" 
64      width="400" 
65      height="300"
66      style="border: 1px solid #ccc"
67      data-on:mousedown="@startDrawing"
68      data-on:mousemove="@draw"
69      data-on:mouseup="@stopDrawing"
70      data-on:mouseleave="@stopDrawing">
71    </canvas>
72  </div>
73</template>

Validation with Codecs #

Rocket’s built-in codec system makes it possible to validate user input. By defining validation rules directly in your data-props:* attributes, data is automatically transformed and validated as it flows through your component.

Type Codecs #

Type codecs convert and validate prop values.

 1<template data-rocket:validated-form
 2          data-props:email="string|trim|required!|="
 3          data-props:age="int|min:18|max:120|=0"
 4          data-props:score="int|clamp:0,100|=0"
 5>
 6  <script>
 7    // Signals are automatically validated by the codec system
 8    // No need for manual codec setup - just use the signals directly
 9    
10    // Check for validation errors using the built-in $$hasErrs signal
11    // No need to create computed - $$hasErrs is automatically available
12  </script>
13  
14  <form>
15    <div>
16      <label>Email (required):</label>
17      <input type="email" data-bind="email">
18      <span data-show="$$errs?.email" class="error">Email is required</span>
19    </div>
20    
21    <div>
22      <label>Age (18-120):</label>
23      <input type="number" data-bind="age">
24      <span data-show="$$errs?.age" class="error">Age must be 18-120</span>
25    </div>
26    
27    <div>
28      <label>Score (0-100, auto-clamped):</label>
29      <input type="number" data-bind="score">
30      <span>Current: <span data-text="$$score"></span></span>
31    </div>
32    
33    <button type="submit" data-attr:disabled="$$hasErrors">
34      Submit
35    </button>
36  </form>
37</template>

Validation Rules #

Codecs can either transform values (modify them) or validate them (check them without modifying). Use the ! suffix to make any codec validation-only.

CodecTransformValidation
Type Conversion
stringConverts to stringIs string?
intConverts to integerIs integer?
floatConverts to numberIs numeric?
dateConverts ISO strings or timestamps to a Date objectIs valid date?
boolConverts to booleanIs boolean?
jsonParses JSON stringValid JSON?
jsParses JS object literal
⚠️ Avoid client values
Valid JS syntax?
binaryDecodes base64Valid base64?
Validation
required-Not empty?
oneOf:a,b,cDefaults to first option if invalidIs valid option?
Numeric Constraints
min:nClamp to minimum value>= minimum?
max:nClamp to maximum value<= maximum?
clamp:min,maxClamp between min and maxIn range?
round / round:nRound to n decimal placesIs rounded?
ceil:n / floor:nCeiling/floor to n decimal placesIs ceiling/floor?
String Transforms
trimRemove leading/trailing whitespace-
upper / lowerConvert to upper/lowercase-
kebab / camelConvert case styleCorrect case?
snake / pascalConvert case styleCorrect case?
title / title:firstTitle case (all words or first only)-
String Constraints
minLength:n-Length >= n?
maxLength:nTruncates if too longLength <= n?
length:n-Length equals n?
regex:pattern-Matches regex?
startsWith:textAdds prefix if missingStarts with text?
endsWith:textAdds suffix if missingEnds with text?
includes:text-Contains text?
Advanced Numeric
lerp:min,maxLinear interpolation (0-1 to min-max)-
fit:in1,in2,out1,out2Map value from one range to another-

Component Lifecycle #

Rocket components have a simple lifecycle with automatic cleanup.

 1<template data-rocket:lifecycle-demo>
 2  <script>
 3    console.log('Component initializing...')
 4    
 5    $$mounted = true
 6    
 7    // Setup effects and timers
 8    const intervalId = setInterval(() => {
 9      console.log('Component is alive')
10    }, 5000)
11    
12    // Cleanup when component is removed from DOM
13    onCleanup(() => {
14      console.log('Component cleanup')
15      clearInterval(intervalId)
16      $$mounted = false
17    })
18  </script>
19  
20  <div>
21    <p data-show="$$mounted">Component is mounted</p>
22  </div>
23</template>

The lifecycle is as follows:

  1. Rocket processes your template and registers the component.
  2. When you add it to the DOM, the instance is created and setup scripts run to initialize your signals.
  3. The component becomes reactive and responds to data changes.
  4. When you remove it from the DOM, all onCleanup callbacks run automatically.

Optimistic UI #

Rocket pairs seamlessly with Datastar’s server-driven model to provide instant visual feedback without shifting ownership of state to the browser. In the Rocket flow example, dragging a node instantly renders its optimistic position in the SVG while the original light-DOM host remains hidden. The component adds an .is-pending class to dim the node and connected edges, signaling that the drag is provisional. Once the backend confirms the new coordinates and updates the layout, the component automatically clears the pending style.

A dedicated prop such as server-update-time="date|=…" makes this straightforward: each tab receives an updated timestamp from the server (via SSE or a patch), Rocket decodes it into a Date, and internal effects react to reconcile every view. Unlike client-owned graph editors (e.g. React Flow), the server stays the single source of truth, while the optimistic UI remains a thin layer inside the component.

Examples #

Check out the Copy Button as a basic example, the QR Code generator with validation, the ECharts integration for data visualization, the interactive 3D Globe with markers, and the Virtual Scroll example for handling large datasets efficiently.