sipperjs
v0.1.0
Published
A reactive UI framework with 3 APIs: state(), control(), snapshot(). SSR-first, no VDOM, no hooks.
Maintainers
Readme
Sipper
A reactive UI framework with 3 APIs. No VDOM. No hooks. No lifecycle methods. SSR-first.
npm install sipperjsThe Entire API
import { state, control, snapshot } from 'sipperjs'| API | What it does |
|-----|-------------|
| state(value) | Create state → [getter, setter] |
| control(fn) | Lifecycle — setup, reactive subscriptions, cleanup |
| snapshot(value) | Escape hatch — freeze a value |
Derived values are plain functions. Components run once. No re-renders.
Quick Start
import { h, state, control, mount } from 'sipperjs'
function Counter() {
const [getCount, setCount] = state(0)
const doubled = getCount() * 2 // auto-reactive (build transform)
return (
<div>
<p>Count: {getCount()}</p>
<p>Doubled: {doubled}</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
</div>
)
}
mount(Counter, document.getElementById('app'))How It Works
State
const [getCount, setCount] = state(0)
getCount() // always returns the latest value
setCount(5) // set directly
setCount(c => c + 1) // updater functionObjects with Immer-Style Updates
const [getUser, setUser] = state({ name: 'V', settings: { theme: 'dark' } })
setUser(draft => {
draft.settings.theme = 'light' // looks like mutation, contained in setter
})Derived Values — Just Functions
const doubled = getCount() * 2 // build transform makes this reactiveThe build transform wraps it: const doubled = () => getCount() * 2. Cascades like a spreadsheet.
Control — The Single Lifecycle
// Setup (runs once, no reactive deps)
control(() => {
console.log('component alive')
return () => console.log('component gone') // cleanup on unmount
})
// Reactive subscription
control(() => {
document.title = `Count: ${getCount()}` // re-runs when count changes
})
// External resources
control(() => {
const id = setInterval(() => tick(), 1000)
return () => clearInterval(id) // auto-cleaned on unmount
})Snapshot — Escape Hatch
const initial = snapshot(getCount()) // frozen at current value, never updatesProvide / Inject
Tree-scoped state without prop drilling:
// Parent
provide('theme', getTheme)
// Any descendant — no matter how deep
const getTheme = inject('theme')SSR
// Server
import { renderToString } from 'sipperjs/server'
const { html, state } = renderToString(App)
// Client
import { hydrate } from 'sipperjs'
hydrate(App, document.getElementById('app'))Vite Setup
// vite.config.js
import reactiveJSX from 'sipperjs/plugin'
export default {
plugins: [reactiveJSX()],
esbuild: { jsx: 'preserve' }
}DevTools
import { initDevTools } from 'sipperjs/devtools'
initDevTools() // adds live state inspector panelDesign Philosophy
- 3 APIs, not 30 —
state()replaces useState/useRef/useMemo/createSignal/ref/reactive/computed - JavaScript is the abstraction — functions are derivations, closures are refs
- Components run once — no re-renders, no VDOM diffing
- SSR-first — server renders HTML, client hydrates with restored state
- The framework prevents mistakes — automatic cleanup, ownership scopes, immutable getters
- Event delegation — one listener per event type, not per element
License
MIT
