Skip to content

Custom Reactive Runtime

Rasen can work with any reactive system. This guide shows how to create custom adapters.

The ReactiveRuntime Interface

typescript
interface ReactiveRuntime {
  watch<T>(
    source: () => T,
    callback: (value: T, oldValue: T) => void,
    options?: { immediate?: boolean; deep?: boolean }
  ): () => void

  effectScope(): {
    run<T>(fn: () => T): T | undefined
    stop(): void
  }

  ref<T>(value: T): Ref<T>
  computed<T>(getter: () => T): ReadonlyRef<T>
  unref<T>(value: T | Ref<T>): T
  isRef(value: unknown): boolean
}

Example: Solid.js Adapter

typescript
import type { ReactiveRuntime } from '@rasenjs/core'
import { createSignal, createEffect, createMemo, onCleanup } from 'solid-js'

export function createSolidRuntime(): ReactiveRuntime {
  return {
    watch(source, callback, options) {
      let oldValue: any
      let isFirst = true
      
      createEffect(() => {
        const newValue = source()
        if (!isFirst || options?.immediate) {
          callback(newValue, oldValue)
        }
        oldValue = newValue
        isFirst = false
      })
      
      // Solid handles cleanup automatically via onCleanup
      return () => {}
    },

    effectScope() {
      let disposed = false
      const cleanups: (() => void)[] = []
      
      return {
        run: <T>(fn: () => T): T | undefined => {
          if (disposed) return undefined
          return fn()
        },
        stop: () => {
          disposed = true
          cleanups.forEach(fn => fn())
        }
      }
    },

    ref<T>(value: T) {
      const [get, set] = createSignal(value)
      return {
        get value() { return get() },
        set value(v: T) { set(() => v) }
      }
    },

    computed<T>(getter: () => T) {
      const memo = createMemo(getter)
      return {
        get value() { return memo() }
      }
    },

    unref<T>(value: T | { value: T }): T {
      if (this.isRef(value)) {
        return (value as { value: T }).value
      }
      return value as T
    },

    isRef(value: unknown): boolean {
      return value !== null && 
             typeof value === 'object' && 
             'value' in value
    }
  }
}

Example: MobX Adapter

typescript
import type { ReactiveRuntime } from '@rasenjs/core'
import { observable, computed as mobxComputed, autorun, runInAction } from 'mobx'

export function createMobXRuntime(): ReactiveRuntime {
  return {
    watch(source, callback, options) {
      let oldValue: any
      let isFirst = true
      
      const disposer = autorun(() => {
        const newValue = source()
        if (!isFirst || options?.immediate) {
          callback(newValue, oldValue)
        }
        oldValue = newValue
        isFirst = false
      })
      
      return disposer
    },

    effectScope() {
      const disposers: (() => void)[] = []
      let stopped = false
      
      return {
        run: <T>(fn: () => T): T | undefined => {
          if (stopped) return undefined
          return fn()
        },
        stop: () => {
          stopped = true
          disposers.forEach(d => d())
        }
      }
    },

    ref<T>(value: T) {
      const box = observable.box(value)
      return {
        get value() { return box.get() },
        set value(v: T) { runInAction(() => box.set(v)) }
      }
    },

    computed<T>(getter: () => T) {
      const comp = mobxComputed(getter)
      return {
        get value() { return comp.get() }
      }
    },

    unref<T>(value: T | { value: T }): T {
      if (this.isRef(value)) {
        return (value as { value: T }).value
      }
      return value as T
    },

    isRef(value: unknown): boolean {
      return value !== null && 
             typeof value === 'object' && 
             'value' in value
    }
  }
}

Usage

typescript
import { setReactiveRuntime } from '@rasenjs/core'
import { createSolidRuntime } from './solid-adapter'

setReactiveRuntime(createSolidRuntime())

Implementation Tips

1. Handle immediate Option

typescript
watch(source, callback, options) {
  if (options?.immediate) {
    callback(source(), undefined)
  }
  // Set up watching...
}

2. Return Stop Handle

typescript
watch(source, callback) {
  const cleanup = setupEffect(...)
  return () => cleanup()
}

3. Ref Interface

Ensure your ref implementation has a getter and setter:

typescript
ref<T>(value: T) {
  return {
    get value() { /* read */ },
    set value(v: T) { /* write */ }
  }
}

4. Computed Is Read-Only

typescript
computed<T>(getter: () => T) {
  return {
    get value() { return getter() }
    // No setter!
  }
}

5. isRef Detection

typescript
isRef(value: unknown): boolean {
  // Check for your specific reactive primitive
  // or fall back to duck typing
  return value !== null && 
         typeof value === 'object' && 
         'value' in value
}

Released under the MIT License.