Reactivity in Twiggle
Twiggle's reactivity is intentionally small and explicit. It centers on two primitives:
- createState(initial) ->
{ get, set } - runSideEffect(fn) -> unsubscribe
The design goals are: predictable updates, explicit reads (no hidden proxies), and cheap subscriptions.
API Contract
-
createState(initialValue)
- Input: any serializable value (primitive or object).
- Output: object with
get()andset(newValue). - Error modes:
setshould accept the same shape as the initial value; passing functions for functional updates is supported (see examples).
-
runSideEffect(fn)
- Input: a function which may call state
getmethods. - Output: a cleanup/unsubscribe function.
- Behavior: the effect runs immediately; any state
getcalls during the run are recorded as dependencies. When any of those states change, the effect re-runs.
- Input: a function which may call state
Basic usage
import { createState } from 'twiggle'
const counter = createState(0)
console.log(counter.get()) // 0
counter.set(1)
console.log(counter.get()) // 1
How UI updates work
When you use get() inside a component's rendering function (JSX), the vite-plugin-twiggle transforms that read into a tracked read. At runtime the renderer subscribes the component to the state. On set(), Twiggle schedules a synchronous (microtask-sized) update for the affected components/effects and updates only the relevant parts of the DOM.
This model gives fine-grained reactive updates without a global reconciliation pass.
runSideEffect details
runSideEffect is like a minimal effect hook
- It runs the provided function immediately.
- During the run any
get()calls register a dependency between the effect and the state. - When a dependency updates, the effect re-runs.
const unsub = runSideEffect(() => {
console.log('value:', counter.get())
})
Edge cases and best practices
- Avoid mutating objects stored in state directly. Instead, set a new object reference so subscribers detect the change.
- Keep expensive computations outside render paths; use cached derived state when necessary.
Implementation notes (for contributors)
- State objects are lightweight and keep a set of subscribers (effects/components) which are called when
setupdates the value. - The plugin's compile-time transformation wraps
get()calls so the renderer/effect runner can register dependencies during rendering or effect execution.
Troubleshooting
- If your component doesn't update, ensure the
get()call happens during the JSX render path (reads outside render won't be tracked automatically). - If derived state doesn't update, verify that the
runSideEffectfunction is created at module/component initialization and that it reads the dependent state usingget().