A high-performance framework with fine-grained observable-based reactivity for building rich applications. Woby is built upon the Soby reactive core, providing an enhanced API for component-based development.
This works similarly to Solid, but without a custom Babel transform and with a different API.
- No VDOM: there's no VDOM overhead, the framework deals with raw DOM nodes directly.
- No stale closures: functions are always executed afresh, no need to worry about previous potential executions of the current function, ever.
- No rules of hooks: hooks are regular functions that can be nested indefinitely, called conditionally, and used outside components, providing maximum flexibility for developers.
- No dependencies arrays: the framework is able to detect what depends on what else automatically, no need to specify dependencies manually.
- No props diffing: updates are fine grained, there's no props diffing, whenever an attribute/property/class/handler/etc. should be updated it's updated directly and immediately.
- No key prop: developers can map over arrays directly or use the
Forcomponent with an array of unique values, eliminating the need to specify keys explicitly. - No Babel: this framework works with plain JavaScript (plus JSX support), eliminating the need for Babel transforms. As a result, there are zero transform function bugs since no code transformation is required.
- No magic: Woby follows a transparent approach where your code behaves exactly as written, with no hidden transformations or unexpected behavior.
- Client-focused: this framework is currently focused on client-side rich applications. Server-related features such as hydration, server components, SSR, and streaming are not implemented at this time.
- Observable-based: observables are at the core of the reactivity system. While the approach differs significantly from React-like systems and may require an initial learning investment, it provides substantial benefits in terms of performance and developer experience.
- Minimal dependencies: Woby is designed with a focus on minimal third-party dependencies, providing a streamlined API for developers who prefer a lightweight solution. The framework draws inspiration from Solid while offering its own unique approach to reactive programming.
- Built-in Class Management: Woby includes powerful built-in class management that supports complex class expressions similar to
classnamesandclsxlibraries, with full reactive observable support. - Web Components Support: First-class support for creating and using custom elements with reactive properties.
- Advanced Context API: Powerful context system that works seamlessly with both JSX components and custom elements.
- Advanced Nested Property Support: Unique feature allowing deeply nested properties to be set directly through HTML attributes using both
$and.notation - a capability not available in React or SolidJS.
📖 Complete Documentation Wiki - Comprehensive guides, tutorials, and API reference
- Installation Guide - Get started with Woby
- Quick Start Tutorial - Build your first app
- API Reference - Complete API documentation
- Reactive Utilities - Working with observables and the
$$function - Reactivity System - Understanding Woby's reactivity model
- Examples Gallery - Practical examples and patterns
- Class Management - Advanced class handling with reactive support
- Best Practices - Recommended patterns and practices
- Woby vs React - API differences and migration guide
- FAQ - Common questions and answers
- Type Synchronization - How HTML attributes sync with component props
- Simple Type Synchronization - Straightforward approach to type sync
- Context API - Advanced context management for components and custom elements
- Custom Elements - Creating and using Web Components with Woby
Woby provides a powerful Context API that works seamlessly with both JSX components and custom elements:
// Create a context
const ThemeContext = createContext('light')
// Use in JSX components
const ThemedButton = () => {
const theme = useContext(ThemeContext)
return <button className={theme}>Themed Button</button>
}
// Use in custom elements
const ThemedElement = defaults(() => ({}), () => {
const [theme, mount] = useMountedContext(ThemeContext)
return <div>{mount}Theme: {theme}</div>
})
customElement('themed-element', ThemedElement)Learn more about the Context API
Woby provides first-class support for creating custom HTML elements with reactive properties:
// Define a component with default props
const Counter = defaults(() => ({
value: $(0, { type: 'number' } as const),
title: $('Counter')
}), ({ value, title }) => (
<div>
<h1>{title}</h1>
<p>Count: {value}</p>
<button onClick={() => value(prev => prev + 1)}>+</button>
</div>
))
// Register as a custom element
customElement('counter-element', Counter)
// Use in JSX or HTML
// JSX: <counter-element value={5} title="My Counter" />
// HTML: <counter-element value="5" title="My Counter"></counter-element>One of Woby's unique features is its advanced nested property support, allowing you to set deeply nested properties directly through HTML attributes:
<!-- Set nested properties using $ notation (works in both HTML and JSX) -->
<user-card
user$name="John Doe"
user$details$age="30"
style$font-size="1.2em"
style$color="blue">
</user-card>
<!-- Set nested properties using . notation (HTML only) -->
<user-card
user.name="John Doe"
user.details.age="30"
style.font-size="1.2em"
style.color="blue">
</user-card>This feature is not available in React or SolidJS, making Woby a more powerful choice for creating highly configurable custom elements that can be used directly in HTML.
Learn more about Custom Elements
You can find some demos and benchmarks below, more demos are contained inside the repository.
- Playground: https://codesandbox.io/s/playground-7w2pxg
- Benchmark: https://krausest.github.io/js-framework-benchmark/current.html
- Counter: https://codesandbox.io/s/demo-counter-23fv5
- Clock: https://codesandbox.io/s/demo-clock-w1e7yb
- Emoji Counter: https://codesandbox.io/s/demo-emoji-counter-j91iz2
- HyperScript: https://codesandbox.io/s/demo-hyperscript-h4rf38
- HTML Template Literal: https://codesandbox.io/s/demo-html-lvfeyo
- Single-file HTML: https://codesandbox.io/s/demo-html-dueygt?file=/public/index.html
- Spiral: https://codesandbox.io/s/demo-spiral-ux33p6
- Store Counter: https://codesandbox.io/s/demo-store-counter-kvoqrw
- Triangle: https://codesandbox.io/s/demo-triangle-l837v0
- Boxes: https://codesandbox.io/s/demo-boxes-wx6rqb
When using AI coding assistants to help with Woby development, please provide these explicit instructions to ensure proper understanding of Woby patterns:
-
Framework Specific Instructions: "Use Woby patterns, NOT React patterns. Woby uses observables with
$()and$$()functions, not React hooks like useState, useEffect with dependency arrays, or useCallback." -
Observable-Based Reactivity: "In Woby, declare reactive variables with
$(), and access their values with$()in reactive contexts. Woby automatically tracks dependencies when you use$()." -
No Dependency Arrays: "Woby hooks like
useEffectanduseMemodo not require dependency arrays. Dependencies are automatically tracked when accessed with$()." -
Reactive Patterns: "Direct observable passing (
{userName}) is preferred for simple reactive content. For complex expressions, use function expressions ({() => $(userName)}) which are automatically tracked."
// ❌ Anti-pattern: React-style useState
const [count, setCount] = useState(0)
// ✅ Woby pattern
const count = $(0)
// ❌ Anti-pattern: React useEffect with dependency array
useEffect(() => {
console.log(count)
}, [count])
// ✅ Woby pattern
useEffect(() => {
console.log($(count))
})
// ❌ Anti-pattern: Non-reactive content
<div>Hello {$(userName)}</div>
// ✅ Woby pattern
<div>Hello {userName}</div>
// ❌ Anti-pattern: React-style array mapping
{todos.map(todo => <div>{todo.text}</div>)}
// ✅ Woby pattern
<For values={todos}>{(todo) => <div>{todo.text}</div>}</For>Contributions are welcome! Please read our contributing guidelines before submitting pull requests.
- S: for pioneering reactive programming approaches that inspired this framework.
- sinuous/observable: for providing an excellent Observable implementation that served as the foundation for this library.
- solid: for serving as a reference implementation, popularizing signal-based reactivity, and building a strong community.
- solid: for serving as a reference implementation, popularizing signal-based reactivity, and building a strong community.
- trkl: for demonstrating the power of minimal, focused implementations.
MIT } return currentTodos })
const activeCount = useMemo(() => { return $$(todos).filter(todo => !todo.completed).length })
return (
{/* Add new todo */}
<div class="flex gap-2 mb-4">
<input
type="text"
value={input}
onInput={(e) => input(e.target.value)}
placeholder="Add a new todo..."
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
class="flex-1 p-2 border border-gray-300 rounded"
/>
<button
onClick={addTodo}
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Add
</button>
</div>
{/* Filter buttons */}
<div class="flex justify-center gap-2 mb-4">
{['all', 'active', 'completed'].map((filterName) => (
<button
key={filterName}
onClick={() => setFilter(filterName)}
class={[
'px-3 py-1 rounded',
() => $$(filter) === filterName
? 'bg-blue-500 text-white'
: 'bg-gray-200 hover:bg-gray-300'
]}
>
{filterName.charAt(0).toUpperCase() + filterName.slice(1)}
</button>
))}
</div>
{/* Todo list */}
<ul class="list-none p-0">
<For values={filteredTodos}>
{(todo) => (
<li class={[
'flex items-center p-2 border-b border-gray-200 gap-2',
{
'line-through text-gray-500': todo.completed,
'bg-yellow-50': () => todo.id % 2 === 0 // Alternate row styling
}
]}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
class="w-4 h-4"
/>
<span class="flex-1">{todo.text}</span>
<button
class="w-6 h-6 flex items-center justify-center bg-red-500 text-white rounded-full hover:bg-red-600 text-xs"
onClick={() => removeTodo(todo.id)}
>
✕
</button>
</li>
)}
</For>
</ul>
{/* Stats */}
<div class="mt-4 p-2 bg-gray-100 rounded text-sm">
Total: {() => $$(todos).length} |
Active: {activeCount} |
Completed: {() => $$(todos).filter(t => t.completed).length}
</div>
</div>
) }
// Render the application render(, document.getElementById('app')!)
## React Compatibility Guide
### useState → Observable
```tsx
// React
const [count, setCount] = useState(0)
// Woby
const count = $(0)
// To update: count(1) or count(prev => prev + 1)
// React
useEffect(() => {
console.log(count)
}, [count])
// Woby
useEffect(() => {
console.log($(count))
})
// No dependency array needed!// React
const doubled = useMemo(() => count * 2, [count])
// Woby
const doubled = useMemo(() => $$(count) * 2)
// No dependency array needed!// React
{isLoggedIn && <div>Welcome!</div>}
// Woby
<If when={isLoggedIn}>
<div>Welcome!</div>
</If>// React
{todos.map(todo => <div key={todo.id}>{todo.text}</div>)}
// Woby
<For values={todos}>
{(todo) => <div>{todo.text}</div>}
</For>| React Type | Woby Equivalent | Description |
|---|---|---|
React.ReactNode |
JSX.Child |
Represents any renderable content |
React.FC<Props> |
JSX.ComponentFunction<Props> |
Function component type |
React.ComponentType<Props> |
JSX.Component<Props> |
Union of function components and intrinsic elements |
React.PropsWithChildren<Props> |
Props & { children?: JSX.Child } |
Props interface with children |
React.Ref<T> |
JSX.Ref<T> |
Ref type definition |
React.MutableRefObject<T> |
Direct DOM access or observable refs | Ref object equivalent |
React.Context<T> |
Woby.Context<T> |
Context object (see createContext) |
React.Dispatch<React.SetStateAction<T>> |
Observable setter pattern | State update function |
React.HTMLProps<T> |
JSX.HTMLAttributes<T> |
HTML element props |
React.CSSProperties |
JSX.CSSProperties |
CSS properties object |
For a comprehensive guide on React to Woby type conversions, see our React to Woby Type Conversion Guide.
Woby provides a set of HTML utility types that make it easier to work with common HTML attribute patterns in custom elements. These utilities implement the ObservableOptions interface and provide consistent conversion between JavaScript values and HTML attributes.
| Woby Utility | Description |
|---|---|
HtmlBoolean |
Handles boolean values with automatic conversion |
HtmlNumber |
Handles numeric values with automatic conversion |
HtmlDate |
Handles Date values with ISO string serialization |
HtmlBigInt |
Handles BigInt values with automatic conversion |
HtmlObject |
Handles Object values with JSON serialization |
HtmlLength |
Handles CSS length values (px, em, rem, %, etc.) |
HtmlBox |
Handles CSS box values (margin, padding, border, etc.) |
HtmlColor |
Handles CSS color values (hex, rgb, etc.) |
HtmlStyle |
Handles CSS style values (objects and strings) |
``tsx import { $, defaults, customElement, HtmlBoolean, HtmlNumber, HtmlColor, HtmlStyle } from 'woby'
interface CounterProps { count?: number enabled?: boolean color?: string styles?: Record<string, string | number> }
const def = () => ({
count:
const Counter = defaults(def, (props: CounterProps) => {
const { count, enabled, color, styles } = props
return (
<div style={() => ({ color:
// Register as custom element customElement('styled-counter', Counter)
### Benefits of HTML Utility Types
1. **Type Safety**: Each utility provides proper type conversion between HTML attributes and JavaScript values
2. **Consistency**: All utilities follow the same pattern and behavior
3. **Automatic Serialization**: Complex values are automatically serialized to/from HTML attributes
4. **Error Handling**: Utilities handle edge cases and invalid values gracefully
5. **Empty String Handling**: All utilities treat empty strings as `undefined` for consistent behavior
6. **Equality Checking**: Each utility implements proper equality checking for value comparison
## Performance Tips
1. **Use Direct Observable Passing**: For simple reactive content, pass observables directly rather than using `$()` in functions
2. **Group Related Effects**: Separate unrelated concerns into individual effects for better performance
3. **Use Early Returns**: Skip unnecessary work in effects when dependencies haven't changed meaningfully
4. **Choose the Right List Component**: Use `For` for objects, `ForValue` for primitives, `ForIndex` for fixed-size lists
5. **Avoid Unnecessary useMemo**: Simple expressions with `() =>` are automatically tracked and often don't need `useMemo`
## APIs
| Core Methods | Components | Hooks | Types & Utilities | Miscellaneous |
|------------------------------------|---------------------------|-----------------------------------|------------------------------------|--------------------------|
| [`](#methods) | [`Dynamic`](#dynamic) | [`useAbortController`](#useabortcontroller) | [`Context`](#context) | [`Contributing`](#contributing) |
| [`batch`](#batch) | [`ErrorBoundary`](#errorboundary) | [`useAbortSignal`](#useabortsignal) | [`Directive`](#directive) | [`Globals`](#globals) |
| [`createContext`](#createcontext) | [`For`](#for) | [`useAnimationFrame`](#useanimationframe) | [`DirectiveOptions`](#directiveoptions) | [`JSX`](#jsx) |
| [`createDirective`](#createdirective) | [`ForIndex`](#forindex) | [`useAnimationLoop`](#useanimationloop) | [`FunctionMaybe`](#functionmaybe) | [`Tree Shaking`](#tree-shaking) |
| [`customElement`](#customelement) | [`ForValue`](#forvalue) | [`useBoolean`](#useboolean) | [`Observable`](#observable) | [`TypeScript`](#typescript) |
| [`createElement`](#createelement) | [`Fragment`](#fragment) | [`useCleanup`](#usecleanup) | [`ObservableReadonly`](#observablereadonly) | |
| [`h`](#h) | [`If`](#if) | [`useContext`](#usecontext) | [`ObservableMaybe`](#observablemaybe) | |
| [`html`](#html) | [`Portal`](#portal) | [`useDisposed`](#usedisposed) | [`ObservableOptions`](#observableoptions) | |
| [`isBatching`](#isbatching) | [`Suspense`](#suspense) | [`useEffect`](#useeffect) | [`Resource`](#resource) | |
| [`isObservable`](#isobservable) | [`Switch`](#switch) | [`useError`](#useerror) | [`StoreOptions`](#storeoptions) | |
| [`isServer`](#isserver) | [`Tary`](#ternary) | [`useEventListener`](#useeventlistener) | | |
| [`isStore`](#isstore) | | [`useFetch`](#usefetch) | | |
| [`lazy`](#lazy) | | [`useIdleCallback`](#useidlecallback) | | |
| [`render`](#render) | | [`useIdleLoop`](#useidleloop) | | |
| [`renderToString`](#rendertostring) | | [`useInterval`](#useinterval) | | |
| [`resolve`](#resolve) | | [`useMemo`](#usememo) | | |
| [`store`](#store) | | [`useMicrotask`](#usemicrotask) | | |
| [`template`](#template) | | [`usePromise`](#usepromise) | | |
| [`untrack`](#untrack) | | [`useReaction`](#usereaction) | | |
| | [`useReadonly`](#usereadonly) | | |
| | [`useResolved`](#useresolved) | | |
| | [`useResource`](#useresource) | | |
| | [`useRoot`](#useroot) | | |
| | [`useSelector`](#useselector) | | |
| | [`useTimeout`](#usetimeout) | | |
## Usage
Woby serves as a view layer built on top of the Observable library [`soby`](https://github.com/wobyjs/soby). Understanding how soby works is essential for effectively using Woby.
Woby re-exports all soby functionality with interfaces adjusted for component and hook usage, along with additional framework-specific functions.
### Counter Example
Here's a complete counter example that demonstrates Woby's reactive capabilities:
**Source:** [@woby/demo](https://github.com/wobyjs/demo) ⭐
```tsx
import { $, $, useMemo, render, Observable, customElement, ElementAttributes } from 'woby'
const Counter = ({ increment, decrement, value, ...props }: {
increment: () => number,
decrement: () => number,
value: Observable<number>
}): JSX.Element => {
const v = $('abc')
const m = useMemo(() => {
return $$(value) + $$(v)
})
return <div {...props}>
<h1>Counter</h1>
<p>{value}</p>
<p>{m}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
}
// Register as custom element
customElement('counter-element', Counter, 'value', 'class', 'style-*')
declare module 'woby' {
namespace JSX {
interface IntrinsicElements {
'counter-element': ElementAttributes<typeof Counter>
}
}
}
const App = () => {
const value = $(0)
const increment = () => value(prev => prev + 1)
const decrement = () => value(prev => prev - 1)
return <counter-element
value={value}
increment={increment}
decrement={decrement}
class="border-2 border-black border-solid bg-amber-400"
/>
}
render(<App />, document.getElementById('app'))
Output:
<counter-element value="0" class="border-2 border-black border-solid bg-amber-400">
<div class="border-2 border-black border-solid bg-amber-400">
<h1>Counter</h1>
<p>0</p>
<p>0abc</p>
<button>+</button>
<button>-</button>
</div>
</counter-element>
Modifying the value attribute on triggers an immediate update to its associated observable.
Woby provides powerful built-in class management that supports complex class expressions with full reactive observable support, similar to popular libraries like classnames and clsx.
Woby supports complex class expressions including arrays, objects, and functions:
// Array of classes
<div class={['red', 'bold']}>Text</div>
// Nested arrays
<div class={['red', ['bold', ['italic']]]}>Text</div>
// Mixed types
<div class={[
"red",
() => ($(value) % 2 === 0 ? "bold" : ""),
{ hidden: true, italic: false },
['hello', ['world']]
]}>Complex classes</div>All class expressions support reactive observables that automatically update when values change:
const isActive = $(false)
const theme = $('dark')
// Reactive boolean
<div class={{ active: isActive }}>Toggle me</div>
// Reactive string
<div class={() => `btn btn-${theme()}`}>Themed button</div>
// Complex reactive expression
<div class={[
'base-class',
() => isActive() ? 'active' : 'inactive',
{ 'loading': loadingState() }
]}>Dynamic element</div>Woby supports object syntax for conditional classes where keys are class names and values are boolean conditions:
const error = $(false)
const warning = $(false)
<div class={{
'base': true, // Always applied
'error': error, // Applied when error is truthy
'warning': warning, // Applied when warning is truthy
'success': !error && !warning // Applied when neither error nor warning
}}>Status element</div>Classes can be computed using functions that return class strings or other class expressions:
const count = $(0)
<div class={() => count() > 5 ? 'high-count' : 'low-count'}>
Count: {count}
</div>
// Function returning complex expression
<div class={() => [
'base',
count() > 10 ? 'large' : 'small',
{ 'even': count() % 2 === 0 }
]}>
Dynamic element
</div>Woby's class system provides built-in functionality equivalent to popular libraries:
- Classnames/CLSX-like syntax: Supports all the same patterns as the popular
classnamesandclsxlibraries - Tailwind CSS ready: Works seamlessly with Tailwind CSS class patterns
- No external dependencies: Built-in implementation eliminates the need for external libraries
- Reactive by default: All class expressions automatically update when observables change
- Performance optimized: Efficient implementation that minimizes DOM updates
If you're familiar with clsx, Woby's class system works similarly:
// Instead of: clsx('foo', true && 'bar', 'baz')
<div class={['foo', true && 'bar', 'baz']}>Content</div>
// Instead of: clsx({ foo:true, bar:false, baz:isTrue() })
<div class={{ foo:true, bar:false, baz:isTrue() }}>Content</div>
// Instead of: clsx(['foo', 0, false, 'bar'])
<div class={['foo', 0, false, 'bar']}>Content</div>For advanced Tailwind CSS class merging, you can wrap your expressions with a custom merge function:
import { twMerge } from 'tailwind-merge'
const mergedClass = useMemo(() => twMerge(
'px-4 py-2 bg-blue-500',
isActive() ? 'bg-blue-700' : 'bg-blue-500'
))
<div class={mergedClass}>Merged classes</div>All reactive elements in class expressions should be wrapped in useMemo or arrow functions () => to ensure proper reactivity:
// Correct - wrapped in useMemo
const dynamicClass = useMemo(() => ({
'active': isActive(),
'disabled': isDisabled()
}))
<div class={dynamicClass}>Content</div>
// Correct - wrapped in arrow function
<div class={() => isActive() ? 'active' : 'inactive'}>Content</div>
// Correct - observables automatically handled
<div class={{ 'active': isActive }}>Content</div>The following top-level functions are provided.
This function is just the default export of soby, it can be used to wrap a value in an observable.
No additional methods are attached to this function. Everything that soby attaches to it is instead exported as components and hooks.
Interface:
function $ <T> (): Observable<T | undefined>;
function $ <T> ( value: undefined, options?: ObservableOptions<T | undefined> ): Observable<T | undefined>;
function $ <T> ( value: T, options?: ObservableOptions<T> ): Observable<T>;Usage:
import {$} from 'woby';
// Create an observable without an initial value
$<number> ();
// Create an observable with an initial value
$(1);
// Create an observable with an initial value and a custom equality function
const equals = ( value, valuePrev ) => Object.is ( value, valuePrev );
const o = $( 1, { equals } );
// Create an observable with an initial value and a special "false" equality function, which is a shorthand for `() => false`, which causes the observable to always emit when its setter is called
const oFalse = $( 1, { equals: false } );
// Getter
o (); // => 1
// Setter
o ( 2 ); // => 2
// Setter via a function, which gets called with the current value
o ( value => value + 1 ); // => 3
// Setter that sets a function, it has to be wrapped in another function because the above form exists
const noop = () => {};
o ( () => noop );This function unwraps a potentially observable value.
Interface:
function $ <T> ( value: T ): (T extends ObservableReadonly<infer U> ? U : T);Usage:
import {$} from 'woby';
// Getting the value out of an observable
const o = $(123);
$ ( o ); // => 123
// Getting the value out of a function
$ ( () => 123 ); // => 123
// Getting the value out of an observable but not out of a function
$ ( o, false ); // => 123
$ ( () => 123, false ); // => () => 123
// Getting the value out of a non-observable and non-function
$ ( 123 ); // => 123This function unwraps a potentially observable value. Recent enhancements to Soby (which Woby uses as its reactive core) have added automatic valueOf() and toString() methods to observable functions, making them behave more naturally in JavaScript contexts where primitives are expected.
Interface:
function $$ <T> ( value: T ): (T extends ObservableReadonly<infer U> ? U : T);Usage:
import {$$} from 'woby';
// Getting the value out of an observable
const o = $(123);
$$ ( o ); // => 123
// Getting the value out of a function
$$ ( () => 123 ); // => 123
// Getting the value out of an observable but not out of a function
$$ ( o, false ); // => 123
$$ ( () => 123, false ); // => () => 123
// Getting the value out of a non-observable and non-function
$$ ( 123 ); // => 123Recent enhancements to Soby have added automatic valueOf() and toString() methods to observable functions. These methods use deepResolve() to automatically resolve observables to their current values in various contexts.
The enhancement was implemented in Soby's src/objects/callable.ts by adding the following lines to both readable and writable observable function generators:
fn.valueOf = () => deepResolve(fn)
fn.toString = () => fn.valueOf().toString()This change affects the creation of observable functions, making them behave more naturally in JavaScript contexts where primitives are expected.
Observables now automatically resolve to their values in string contexts:
import {$} from 'woby'
// In template literals
const name = $('John')
console.log(`Hello, ${name}!`) // Outputs: "Hello, John!"
// In JSX expressions
const App = () => {
const count = $(5)
return <div>Count: {count}</div> // Renders: "Count: 5"
}Observables automatically resolve in mathematical operations:
import {$} from 'woby'
const count = $(5)
const result = count + 10 // Results in 15 automatically
const price = $(19.99)
const tax = $(0.08)
const total = price * (1 + tax) // Automatically calculates with current valuesWhen binding observables to DOM attributes, they automatically convert to appropriate string representations:
import {$} from 'woby'
const isVisible = $(true)
const opacity = $(0.5)
// These will automatically convert to appropriate string values
const element = <div hidden={isVisible} style={{ opacity }}>Content</div>The deepResolve function recursively resolves observables, which means for deeply nested structures there could be performance implications in hot paths. The resolution happens every time valueOf() or toString() is called.
For performance-critical applications with deeply nested structures, explicit unwrapping with $$() may be preferred:
// This maintains reactivity by directly passing the observable
const reactive = <div>{deeplyNestedObject}</div>
// This unwraps the observable to get its static value, losing reactivity
const staticValue = <div>{$$(deeplyNestedObject)}</div>
// With the valueOf enhancement, mathematical operations are simplified
const price = $(19.99);
const quantity = $(3);
const total = <div>Total: {() => price * quantity}</div>; // Automatically computes 59.97This enhancement improves rather than breaks existing functionality:
- All existing code continues to work as before
- Explicit unwrapping with
$$()still works and may be preferred in performance-critical situations - The enhancement provides additional convenience without removing any capabilities
This function holds onto updates within its scope and flushes them out once it exits.
Interface:
function batch <T> ( fn: () => T ): T;
function batch <T> ( value: T ): T;Usage:
import {batch} from 'woby';
batch // => Same as require ( 'soby' ).batchThis function creates a context object, optionally with a default value, which can later be used to provide a new value for the context or to read the current value.
A context's Provider will register the context with its children, which is always what you want, but it can lead to messy code due to nesting.
A context's register function will register the context with the current parent observer, which is usually only safe to do at the root level, but it will lead to very readable code.
Interface:
type ContextProvider<T> = ( props: { value: T, children: JSX.Element } ) => JSX.Element;
type ContextRegister<T> = ( value: T ) => void;
type Context<T> = { Provider: ContextProvider<T>, register: ContextRegister<T> };
function createContext <T> ( defaultValue?: T ): Context<T>;Usage:
import {createContext, useContext} from 'woby';
const App = () => {
const Context = createContext ( 123 );
return (
<>
{() => {
const value = useContext ( Context );
return <p>{value}</p>;
}}
<Context.Provider value={312}>
{() => {
const value = useContext ( Context );
return <p>{value}</p>;
}}
</Context.Provider>
</>
);
};This function creates a directive provider, which can be used to register a directive with its children.
A directive is a function that always receives an Element as its first argument, which is basically a ref to the target element, and arbitrary user-provided arguments after that.
Each directive has a unique name and it can be called by simply writing use:directivename={[arg1, arg2, ...argN]]} in the JSX.
Directives internally are registered using context providers, so you can also override directives for a particular scope just by registering another directive with the same name closer to where you are reading it.
A directive's Provider will register the directive with its children, which is always what you want, but it can lead to messy code due to nesting.
A directive's register function will register the directive with the current parent observer, which is usually only safe to do at the root level, but it will lead to very readable code.
Interface:
type DirectiveFunction = <T extends unknown[]> ( ref: Element, ...args: T ) => void;
type DirectiveProvider = ( props: { children: JSX.Element } ) => JSX.Element;
type DirectiveRef<T extends unknown[]> = ( ...args: T ) => (( ref: Element ) => void);
type DirectiveRegister = () => void;
type Directive = { Provider: DirectiveProvider, ref: DirectiveRef, register: DirectiveRegister };
function createDirective <T extends unknown[] = []> ( name: string, fn: DirectiveFunction<T>, options?: DirectiveOptions ): Directive;Usage:
import {createDirective, useEffect} from 'woby';
// First of all if you are using TypeScript you should extend the "JSX.Directives" interface, so that TypeScript will know about your new directive
namespace JSX {
interface Directives {
tooltip: [title: string] // Mapping the name of the directive to the array of arguments it accepts
}
}
// Then you should create a directive provider
const TooltipDirective = createDirective ( 'tooltip', ( ref, title: string ) => {
useEffect ( () => {
if ( !ref () ) return; // The element may not be available yet, or it might have been unmounted
// Code that implements a tooltip for the given element here...
});
});
// Then you can use the new "tooltip" directive anywhere inside the "TooltipDirective.Provider"
const App = () => {
return (
<TooltipDirective.Provider>
<input value="Placeholder..." use:tooltip={['This is a tooltip!']} />
</TooltipDirective.Provider>
);
};
// You can also use directives directly by padding them along as refs
const App = () => {
return <input ref={TooltipDirective.ref ( 'This is a tooltip!' )} value="Placeholder..." />;
};This is the internal function that will make DOM nodes and call/instantiate components, it will be called for you automatically via JSX.
Interface:
function createElement <P = {}> ( component: JSX.Component<P>, props: P | null, ...children: JSX.Element[] ): () => JSX.Element);Usage:
import {createElement} from 'woby';
const element = createElement ( 'div', { class: 'foo' }, 'child' ); // => () => HTMLDivElementThis function is just an alias for the createElement function, it's more convenient to use if you want to use Woby in hyperscript mode just because it has a much shorter name.
Interface:
function h <P = {}> ( component: JSX.Component<P>, props: P | null, ...children: JSX.Element[] ): () => JSX.Element);Usage:
import {h} from 'woby';
const element = h ( 'div', { class: 'foo' }, 'child' ); // => () => HTMLDivElementThis function provides an alternative way to use the framework, without writing JSX or using the h function manually, it instead allows you to write your markup as tagged template literals.
htm is used under the hood, read its documentation.
Interface:
function html ( strings: TemplateStringsArray, ...values: any[] ): JSX.Element;```
Usage:
```tsx
import {html, If} from 'woby';
const Counter = (): JSX.Element => {
const value = $(0);
const increment = () => value ( prev => prev + 1 );
const decrement = () => value ( prev => prev - 1 );
return html`
<h1>Counter</h1>
<p>${value}</p>
<button onClick=${increment}>+</button>
<button onClick=${decrement}>-</button>
`;
};
// Using a custom component without registering it
const NoRegistration = (): JSX.Element => {
return html`
<${If} when=${true}>
<p>content</p>
</${If}>
`;
};
// Using a custom component after registering it, so you won't need to interpolate it anymore
html.register ({ If });
const NoRegistration = (): JSX.Element => {
return html`
<If when=${true}>
<p>content</p>
</If>
`;
};This function tells you if batching is currently active or not.
Interface:
function isBatching (): boolean;Usage:
import {batch, isBatching} from 'woby';
// Checking if currently batching
isBatching (); // => false
batch ( () => {
isBatching (); // => true
});
isBatching (); // => falseThis function tells you if a variable is an observable or not.
Interface:
function isObservable <T = unknown> ( value: unknown ): value is Observable<T> | ObservableReadonly<T>;Usage:
import {$, isObservable} from 'woby';
isObservable ( 123 ); // => false
isObservable ( $(123) ); // => trueThis function tells you if your code is executing in a browser environment or not.
Interface:
function isServer (): boolean;Usage:
import {isServer} from 'woby';
isServer (); // => true or falseThis function tells you if a variable is a store or not.
Interface:
function isStore ( value: unknown ): boolean;Usage:
import {store, isStore} from 'woby';
isStore ( {} ); // => false
isStore ( store ( {} ) ); // => trueThis function creates a lazy component, which is loaded via the provided function only when/if needed.
This function uses useResource internally, so it's significant for Suspense too.
Interface:
type LazyComponent<P = {}> = ( props: P ) => ObservableReadonly<Child>;
type LazyFetcher<P = {}> = () => Promise<{ default: JSX.Component<P> } | JSX.Component<P>>;
type LazyResult<P = {}> = LazyComponent<P> & ({ preload: () => Promise<void> });
function lazy <P = {}> ( fetcher: LazyFetcher<P> ): LazyResult<P>;Usage:
import {lazy} from 'woby';
const LazyComponent = lazy ( () => import ( './component' ) );This function mounts a component inside a provided DOM element and returns a disposer function for unmounting it and stopping all reactivity inside it.
Interface:
function render ( child: JSX.Element, parent?: HTMLElement | null ): Disposer;Usage:
import {render} from 'woby';
const App = () => <p>Hello, World!</p>;
const dispose = render ( <App />, document.body );
dispose (); // Unmounted and all reactivity inside it stoppedThis function operates similarly to render, but returns a Promise that resolves to the HTML representation of the rendered component.
The current implementation works within browser-like environments. For server-side usage, JSDOM or similar solutions are required.
This function automatically waits for all Suspense boundaries to resolve before returning the HTML.
Interface:
function renderToString ( child: JSX.Element ): Promise<string>;Usage:
import {renderToString} from 'woby';
const App = () => <p>Hello, World!</p>;
const html = await renderToString ( <App /> );This function resolves all reactivity within the provided argument, replacing each function with a memo that captures the function's value.
While developers may not need to use this function directly, it is internally necessary to ensure proper tracking of child values by their parent computations.
Interface:
type ResolvablePrimitive = null | undefined | boolean | number | bigint | string | symbol;
type ResolvableArray = Resolvable[];
type ResolvableObject = { [Key in string | number | symbol]?: Resolvable };
type ResolvableFunction = () => Resolvable;
type Resolvable = ResolvablePrimitive | ResolvableObject | ResolvableArray | ResolvableFunction;
function resolve <T> ( value: T ): T extends Resolvable ? T : never;Usage:
import {resolve} from 'woby';
resolve // => Same as require ( 'soby' ).resolveThis function returns a deeply reactive version of the passed object, where property accesses and writes are automatically interpreted as Observables reads and writes for you.
Interface:
function store <T> ( value: T, options?: StoreOptions ): T;Usage:
import {store} from 'woby';
store // => Same as require ( 'soby' ).storeThis function enables constructing elements with Solid-level performance without using the Babel transform, but also without the convenience of that.
This function works similarly to sinuous's template function but provides a cleaner API, as props are accessed identically inside and outside the template.
This function can be used to wrap components that do not directly create observables or call hooks, significantly improving performance during component instantiation.
Interface:
function template <P = {}> ( fn: (( props: P ) => JSX.Element) ): (( props: P ) => () => Element);Usage:
import {template} from 'woby';
const Row = template ( ({ id, cls, label, onSelect, onRemove }) => { // Now Row is super fast to instantiate
return (
<tr class={cls}>
<td class="col-md-1">{id}</td>
<td class="col-md-4">
<a onClick={onSelect}>{label}</a>
</td>
<td class="col-md-1">
<a onClick={onRemove}>
<span class="glyphicon glyphicon-remove" ariaHidden={true}></span>
</a>
</td>
<td class="col-md-6"></td>
</tr>
);
});
const Table = () => {
const rows = [ /* props for all your rows here */ ];
return rows.map ( row => <Row {...row}> );
};This function executes the provided function without creating dependencies on observables retrieved inside it.
Interface:
function untrack <T> ( fn: () => T ): T;
function untrack <T> ( value: T ): T;Usage:
import {untrack} from 'woby';
untrack // => Same as require ( 'soby' ).untrackThe following components are provided.
Crucially some components are provided for control flow, since regular JavaScript control flow primitives are not reactive, and we need to have reactive alternatives to them to have great performance.
This component is just an alternative to createElement that can be used in JSX, it's useful to create a new element dynamically.
Interface:
function Dynamic <P = {}> ( props: { component: ObservableMaybe<JSX.Component<P>, props?: FunctionMaybe<P | null>, children?: JSX.Element }): JSX. Element;Usage:
import {Dynamic} from 'woby';
const App = () => {
const heading = 'h2';
return (
<Dynamic component={heading}>
Some content
</Dynamic>
);
};The error boundary catches errors thrown inside it, and renders a fallback component when that happens.
Interface:
function ErrorBoundary ( props: { fallback: JSX.Element | (( props: { error: Error, reset: Callback } ) => JSX.Element), children: JSX.Element }): ObservableReadonly<JSX.Element>;Usage:
import {ErrorBoundary} from 'woby';
const Fallback = ({ reset, error }: { reset: () => void, error: Error }) => {
return (
<>
<p>Error: {error.message}</p>
<button onClick={reset}>Recover</button>
</>
);
};
const SomeComponentThatThrows = () => {
throw new Error('whatever');
};
const App = () => {
return (
<ErrorBoundary fallback={Fallback}>
<SomeComponentThatThrows />
</ErrorBoundary>
);
};This component is the reactive alternative to natively mapping over an array.
It must be called with an array, or a function that returns an array, of unique values, and each of them are passed to the child function to render something.
Interface:
function For <T> ( props: { values: FunctionMaybe<readonly T[]>, fallback?: JSX.Element, children: (( value: T, index: FunctionMaybe<number> ) => JSX.Element) }): ObservableReadonly<JSX.Element>;Usage:
import {For} from 'woby';
const App = () => {
const numbers = [1, 2, 3, 4, 5];
return (
<For values={numbers}>
{( value ) => {
return <p>Value: {value}</p>
}}
</For>
);
};This component is a reactive alternative to natively mapping over an array, but it takes the index as the unique key instead of the value.
This is an alternative to For that uses the index of the value in the array for caching, rather than the value itself.
It's recommended to use ForIndex for arrays containing duplicate values and/or arrays containing primitive values, and For for everything else.
The passed function will always be called with a read-only observable containing the current value at the index being mapped.
Interface:
type Value<T = unknown> = T extends ObservableReadonly<infer U> ? ObservableReadonly<U> : ObservableReadonly<T>;
function ForIndex <T> ( props: { values: FunctionMaybe<readonly T[]>, fallback?: JSX.Element, children: (( value: Value<T>, index: number ) => JSX.Element) }): ObservableReadonly<JSX.Element>;Usage:
import {ForIndex} from 'woby';
const App = () => {
const numbers = [1, 2, 3, 4, 5];
return (
<ForIndex values={numbers}>
{( value ) => {
return <p>Double value: {() => value () ** 2}</p>
}}
</ForIndex>
);
};This component is a reactive alternative to natively mapping over an array, but it caches results for values that didn't change, and repurposes results for items that got discarded for new items that need to be rendered.
This is an alternative to For and ForIndex that enables reusing the same result for different items, when possible. Reusing the same result means also reusing everything in it, including DOM nodes.
Basically Array.prototype.map doesn't wrap the value nor the index in an observable, For wraps the index only in an observable, ForIndex wraps the value only in an observable, and ForValue wraps both the value and the index in observables.
This is useful for use cases like virtualized rendering, where For would cause some nodes to be discarded and others to be created, ForIndex would cause all nodes to be repurposed, but ForValue allows you to only repurpose the nodes that would have been discareded by For, not all of them.
This is a more advanced component, it's recommended to simply use For or ForIndex, until you really understand how to squeeze extra performance with this, and you actually need that performance.
Interface:
type Value<T = unknown> = T extends ObservableReadonly<infer U> ? ObservableReadonly<U> : ObservableReadonly<T>;
function ForValue <T> ( props: { values: FunctionMaybe<readonly T[]>, fallback?: JSX.Element, children: (( value: Value<T>, index: ObservableReadonly<number> ) => JSX.Element) }): ObservableReadonly<JSX.Element>;Usage:
import {ForValue} from 'woby';
const App = () => {
const numbers = [1, 2, 3, 4, 5];
return (
<ForValue values={numbers}>
{( value ) => {
return <p>Double value: {() => value () ** 2}</p>
}}
</ForValue>
);
};```
#### `Fragment`
This is just the internal component used for rendering fragments: `<></>`, you probably would never use this directly even if you are not using JSX, since you can return plain arrays from your components anyway.
Interface:
```ts
function Fragment ( props: { children: JSX.Element }): JSX.Element;Usage:
import {Fragment} from 'woby';
const App = () => {
return (
<Fragment>
<p>child 1</p>
<p>child 2</p>
</Fragment>
);
};This component is the reactive alternative to the native if.
If a function is passed as the children then it will be called with a read-only observable that contains the current, always truthy, value of the "when" condition.
Interface:
type Truthy<T = unknown> = Extract<T, number | bigint | string | true | object | symbol | Function>;
function If <T> ( props: { when: FunctionMaybe<T>, fallback?: JSX.Element, children: JSX.Element | (( value: (() => Truthy<T>) ) => JSX.Element) }): ObservableReadonly<JSX.Element>;Usage:
import {If} from 'woby';
const App = () => {
const visible = $(false);
const toggle = () => visible ( !visible () );
return (
<>
<button onClick={toggle}>Toggle</button>
<If when={visible}>
<p>Hello!</p>
</If>
</>
);
};This component mounts its children inside a provided DOM element, or inside document.body otherwise.
The mount prop can also be an observable, if its value changes the portal is reparented.
The when prop can be used to apply the portal conditionally, if it explicitly resolves to false then children are mounted normally, as if they weren't wrapped in a portal.
Events will propagate natively, according to the resulting DOM hierarchy, not the components hierarchy.
Interface:
function Portal ( props: { when: boolean, mount?: JSX.Element, wrapper?: JSX.Element, children: JSX.Element }): (() => JSX.Element | null) & ({ metadata: { portal: HTMLDivElement } });Usage:
import Portal from 'woby';
const Modal = () => {
// Some modal component maybe...
};
const App = () => {
return (
<Portal mount={document.body}>
<Modal />
</Portal>
);
};This component is like If, the reactive alternative to the native if, but the fallback branch is shown automatically while there are some resources loading in the main branch, and the main branch is kept alive under the hood.
So this can be used to show some fallback content while the actual content is loading in the background.
This component relies on useResource to understand if there's a resource loading or not.
This component also supports a manual "when" prop for manually deciding whether the fallback branch should be rendered or not.
Interface:
function Suspense ( props: { when?: FunctionMaybe<unknown>, fallback?: JSX.Element, children: JSX.Element }): ObservableReadonly<JSX.Element>;Usage:
import {Suspense} from 'woby';
const App = () => {
const Content = () => {
const resource = useResource ( () => makeSomePromise () );
return (
<If when={() => !resource ().pending && !resource ().error}>
{resource ().value}
</If>
);
};
const Spinner = () => {
return <p>Loading...</p>;
};
return (
<Suspense fallback={<Spinner />}>
<Content />
</Suspense>
);
};This component is the reactive alternative to the native switch.
Interface:
function Switch <T> ( props: { when: FunctionMaybe<T>, fallback?: JSX.Element, children: JSX.Element }): ObservableReadonly<JSX.Element>;
Switch.Case = function <T> ( props: { when: T, children: JSX.Element } ): (() => JSX.Element) & ({ metadata: [T, JSX.Element] });
Switch.Default = function ( props: { children: JSX.Element } ): (() => JSX.Element) & ({ metadata: [JSX.Element] });Usage:
import {Switch} from 'woby';
const App = () => {
const value = $(0);
const increment = () => value ( value () + 1 );
const decrement = () => value ( value () - 1 );
return (
<>
<Switch when={value}>
<Switch.Case when={0}>
<p>0, the boundary between positives and negatives! (?)</p>
</Switch.Case>
<Switch.Case when={1}>
<p>1, the multiplicative identity!</p>
</Switch.Case>
<Switch.Default>
<p>{value}, I don't have anything interesting to say about that :(</p>
</Switch.Default>
</Switch>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</>
);
};This component is the reactive alternative to the native ternary operator.
The first child will be rendered when the condition is truthy, otherwise the second child will be rendered.
Interface:
function Ternary ( props: { when: FunctionMaybe<unknown>, children: [JSX.Element, JSX.Element] } ): ObservableReadonly<JSX.Element>;Usage:
import {Ternary} from 'woby';
const App = () => {
const visible = $(false);
const toggle = () => visible ( !visible () );
return (
<>
<button onClick={toggle}>Toggle</button>
<Ternary when={visible}>
<p>Visible :)</p>
<p>Invisible :(</p>
</Ternary>
</>
);
};The following hooks are provided.
Many of these are just functions that soby provides, re-exported as use* functions, the rest are largely just alternatives to web built-ins that can also accept observables as arguments and can dispose of themselves automatically when the parent computation is disposed.
Hooks are just regular functions, if their name starts with use then we call them hooks just because.
This hook is just an alternative to new AbortController () that automatically aborts itself when the parent computation is disposed.
Interface:
function useAbortController ( signals?: ArrayMaybe<AbortSignal> ): AbortController;Usage:
import {useAbortController} from 'woby';
const controller = useAbortController ();This hook is just a convenient alternative to useAbortController, if you are only interested in its signal, which is automatically aborted when the parent computation is disposed.
Interface:
function useAbortSignal ( signals?: ArrayMaybe<AbortSignal> ): AbortSignal;Usage:
import {useAbortSignal} from 'woby';
const signal = useAbortSignal ();This hook is just an alternative to requestAnimationFrame that automatically clears itself when the parent computation is disposed.
Interface:
function useAnimationFrame ( callback: ObservableMaybe<FrameRequestCallback> ): Disposer;Usage:
import {useAnimationFrame} from 'woby';
useAnimationFrame ( () => console.log ( 'called' ) );This hook is just a version of useAnimationFrame that loops until the parent computation is disposed.
Interface:
function useAnimationLoop ( callback: ObservableMaybe<FrameRequestCallback> ): Disposer;Usage:
import {useAnimationLoop} from 'woby';
useAnimationLoop ( () => console.log ( 'called' ) );This hook is like the reactive equivalent of the !! operator, it returns you a boolean, or a function to a boolean, depending on the input that you give it.
Interface:
function useBoolean ( value: FunctionMaybe<unknown> ): FunctionMaybe<boolean>;Usage:
import {useBoolean} from 'woby';
useBoolean // => Same as require ( 'soby' ).booleanThis hook registers a function to be called when the parent computation is disposed.
Interface:
function useCleanup ( fn: () => void ): void;Usage:
import {useCleanup} from 'woby';
useCleanup // => Same as require ( 'soby' ).cleanupThis hook retrieves the value out of a context object.
Interface:
function useContext <T> ( context: Context<T> ): T | undefined;Usage:
import {createContext, useContext} from 'woby';
const App = () => {
const ctx = createContext ( 123 );
const value = useContext ( ctx );
return <p>{value}</p>;
};This hook returns a boolean read-only observable that is set to true when the parent computation gets disposed of.
Interface:
function useDisposed (): ObservableReadonly<boolean>;Usage:
import {useDisposed} from 'woby';
useDisposed // => Same as require ( 'soby' ).disposedThis hook registers a function to be called when any of its dependencies change. If a function is returned it's automatically registered as a cleanup function.
Interface:
function useEffect ( fn: () => (() => void) | void ): (() => void);Usage:
import {useEffect} from 'woby';
useEffect // => Same as require ( 'soby' ).effectThis hook registers a function to be called when the parent computation throws.
Interface:
function useError ( fn: ( error: Error ) => void ): void;Usage:
import {useError} from 'woby';
useError // => Same as require ( 'soby' ).errorThis hook is just an alternative to addEventListener that automatically clears itself when the parent computation is disposed.
Interface:
function useEventListener ( target: FunctionMaybe<EventTarget>, event: FunctionMaybe<string>, handler: ObservableMaybe<( event: Event ) => void>, options?: FunctionMaybe<true | AddEventListenerOptions> ): Disposer;Usage:
import {useEventListener} from 'woby';
useEventListener ( document, 'click', console.log );This hook wraps the output of a fetch request in an observable, so that you can be notified when it resolves or rejects. The request is also aborted automatically when the parent computation gets disposed of.
This hook uses useResource internally, so it's significant for Suspense too.
Interface:
function useFetch ( request: FunctionMaybe<RequestInfo>, init?: FunctionMaybe<RequestInit> ): ObservableReadonly<Resource<Response>>;Usage:
import {useFetch} from 'woby';
const App = () => {
const state = useFetch ( 'https://my.api' );
return state.on ( state => {
if ( state.pending ) return <p>pending...</p>;
if ( state.error ) return <p>{state.error.message}</p>;
return <p>Status: {state.value.status}</p>
});
};```
#### `useIdleCallback`
This hook is just an alternative to `requestIdleCallback` that automatically clears itself when the parent computation is disposed.
Interface:
```ts
function useIdleCallback ( callback: ObservableMaybe<IdleRequestCallback>, options?: FunctionMaybe<IdleRequestOptions> ): Disposer;Usage:
import {useIdleCallback} from 'woby';
useIdleCallback ( () => console.log ( 'called' ) );This hook is just a version of useIdleCallback that loops until the parent computation is disposed.
Interface:
function useIdleLoop ( callback: ObservableMaybe<IdleRequestCallback>, options?: FunctionMaybe<IdleRequestOptions> ): Disposer;Usage:
import {useIdleLoop} from 'woby';
useIdleLoop ( () => console.log ( 'called' ) );This hook is just an alternative to setInterval that automatically clears itself when the parent computation is disposed.
Interface:
function useInterval ( callback: ObservableMaybe<Callback>, ms?: FunctionMaybe<number> ): Disposer;Usage:
import {useInterval} from 'woby';
useInterval ( () => console.log ( 'called' ), 1000 );This hook is the crucial other ingredient that we need, other than observables themselves, to have a powerful reactive system that can track dependencies and re-execute computations when needed.
This hook registers a function to be called when any of its dependencies change, and the return of that function is wrapped in a read-only observable and returned.
The function receives an optional stack parameter (an Stack object) that provides a debugging stack trace to help pinpoint the source of reactive dependencies. To enable this feature, set DEBUGGER.debug = true. Additionally, you can enable verbose comment debugging by setting DEBUGGER.verboseComment = true.
Interface:
function useMemo <T> ( fn: (stack?: Error) => T, options?: ObservableOptions<T | undefined> ): ObservableReadonly<T>;Usage:
import {useMemo} from 'woby';
// With stack parameter for debugging
useMemo((stack) => {
if (stack) console.log(stack.stack); // Logs stack trace to pinpoint reactive dependencies
return computeExpensiveValue(a, b);
});This hook is just an alternative to queueMicrotask that automatically clears itself when the parent computation is disposed, and that ensures things like contexts, error boundaries etc. keep working inside the microtask.
Interface:
function useMicrotask ( fn: () => void ): void;Usage:
import {useMicrotask} from 'woby';
useMicrotask ( () => console.log ( 'called' ) );This hook wraps a promise in an observable, so that you can be notified when it resolves or rejects.
This hook uses useResource internally, so it's significant for Suspense too.
Interface:
function usePromise <T> ( promise: FunctionMaybe<Promise<T>> ): ObservableReadonly<Resource<T>>;Usage:
import {usePromise} from 'woby';
const App = () => {
const request = fetch ( 'https://my.api' ).then ( res => res.json ( 0 ) );
const promise = usePromise ( request );
return resolved.on ( state => {
if ( state.pending ) return <p>pending...</p>;
if ( state.error ) return <p>{state.error.message}</p>;
return <p>{JSON.stringify ( state.value )}</p>
});
};This hook works just like useEffect, expect that it's not affected by Suspense.
This is an advanced hook mostly useful internally, you may never need to use this, useEffect and useMemo should suffice.
Interface:
function useReaction ( fn: () => (() => void) | void ): (() => void);Usage:
import {useReaction} from 'woby';
useReaction // => Same as require ( 'soby' ).reactionThis hook creates a read-only observable out of another observable.
Interface:
function useReadonly <T> ( observable: Observable<T> | ObservableReadonly<T> ): ObservableReadonly<T>;Usage:
import {useReadonly} from 'woby';
useReadonly // => Same as require ( 'soby' ).readonlyThis hook receives a value, or an array of values, potentially wrapped in functions and/or observables, and unwraps it/them.
If no callback is used then it returns the unwrapped value, otherwise it returns whatever the callback returns.
This is useful for handling reactive and non reactive values the same way. Usually if the value is a function, or always for convenience, you'd want to wrap the useResolved call in a useMemo, to maintain reactivity.
This is potentially a more convenient version of `, made especially for handling nicely arguments passed that your hooks receive that may or may not be observables.
Interface:
The precise interface for this function is insane, you can find it here: https://github.com/wobyjs/woby/blob/master/src/hooks/use_resolved.ts
Usage:
import {$, useResolved} from 'woby';
useResolved ( 123 ); // => 123
useResolved ( $(123) ); // => 123
useResolved ( () => 123 ); // => 123
useResolved ( () => 123, false ); // => () => 123
useResolved ( $(123), value => 321 ); // => 321
useResolved ( [$(123), () => 123], ( a, b ) => 321 ); // => 321This hook wraps the result of a function call with an observable, handling the cases where the function throws, the result is an observable, the result is a promise or an observale that resolves to a promise, and the promise rejects, so that you don't have to worry about these issues.
This basically provides a unified way to handle sync and async results, observable and non observable results, and functions that throw and don't throw.
This function is also the mechanism through which Suspense understands if there are things loading under the hood or not.
When the value property is read while fetching, or when the latest property is read the first time, or after an error, while fetching, then Suspense boundaries will be triggered.
When the value property or the latest property are read after the fetch errored they will throw, triggering ErrorBoundary.
The passed function is tracked and it will be automatically re-executed whenever any of the observables it reads change.
Interface:
function useResource <T> ( fetcher: (() => ObservableMaybe<PromiseMaybe<T>>) ): Resource<T>;Usage:
import {useResource} from 'woby';
const fetcher = () => fetch ( 'https://my.api' );
const resource = useResource ( fetcher );This hook creates a new computation root, detached from any parent computation.
Interface:
function useRoot <T> ( fn: ( dispose: () => void ) => T ): T;Usage:
import {useRoot} from 'woby';
useRoot // => Same as require ( 'soby' ).rootThis hook massively optimizes isSelected kind of workloads.
Interface:
type SelectorFunction<T> = ( value: T ) => ObservableReadonly<boolean>;
function useSelector <T> ( source: () => T | ObservableReadonly<T> ): SelectorFunction<T>;Usage:
import {useSelector} from 'woby';
useSelector // => Same as require ( 'soby' ).selectorThis hook is just an alternative to setTimeout that automatically clears itself when the parent computation is disposed.
Interface:
function useTimeout ( callback: ObservableMaybe<Callback>, ms?: FunctionMaybe<number> ): Disposer;Usage:
import {useTimeout} from 'woby';
useTimeout ( () => console.log ( 'called' ), 1000 );This type describes the object that createContext gives you.
Interface:
type Context<T = unknown> = {
Provider ( props: { value: T, children: JSX.Element } ): JSX.Element,
register ( value: T ): void
};Usage:
import {useContext} from 'woby';
import type {Context} from 'woby';
// Create an alternative useContext that throws if the context is not available
const useNonNullableContext = <T> ( context: Context<T> ): NonNullable<T> => {
const value = useContext ( context );
if ( value === null || value === undefined ) throw new Error ( 'Missing context' );
return value;
};This type describes the object that createDirective gives you.
Interface:
type Directive<Arguments extends unknown[] = []> = {
Provider: ( props: { children: JSX.Element } ) => JSX.Element,
ref: ( ...args: Arguments ) => (( ref: Element ) => void),
register: () => void
};Usage:
import {$, useEffect} from 'woby';
import type {Directive, FunctionMaybe} from 'woby';
// Example hook for turning a directive into a hook
const useDirective = <T extends unknown[] = []> ( directive: Directive<T> ) => {
return ( ref: FunctionMaybe<Element | undefined>, ...args: T ): void => {
useEffect ( () => {
const target = $(ref);
if ( !target ) return;
directive.ref ( ...args )( target );
});
};
};This type describes the options object that the createDirective function accepts.
Interface:
type DirectiveOptions = {
immediate?: boolean // If `true` the directive is called as soon as the node is created, otherwise it also waits for that node to be attached to the DOM
};Usage:
import {createDirective} from 'woby';
// Create an regular, non-immediate, directive
const TooltipDirective = createDirective ( 'tooltip', ( ref, title: string ) => {
// Implementation...
});
// Create an immediate directive
const TooltipDirectiveImmediate = createDirective ( 'tooltip', ( ref, title: string ) => {
// Implementation...
}, { immediate: true } );This type says that something can be the value itself or a function that returns that value.
It's useful at times since some components, like If, accept when conditions wrapped in FunctionMaybe.
Interface:
type FunctionMaybe<T> = (() => T) | T;Usage:
import type {FunctionMaybe} from 'woby';
const SomeConditionalComponent = ( when: FunctionMaybe<boolean>, value: string ): JSX.Element => {
return (
<If when={when}>
{value}
</If>
);
};This type says that something is a regular observable, which can be updated via its setter.
Interface:
type Observable<T> = {
(): T,
( value: T ): T,
( fn: ( value: T ) => T ): T
};Usage:
import type {Observable} from 'woby';
const fn = ( value: Observable<boolean> ): void => {
value (); // Getting
value ( true ); // Setting
};This type says that something is a read-only observable, which can only be read but not updated.
Interface:
type ObservableReadonly<T> = {
(): T
};Usage:
import type {ObservableReadonly} from 'woby';
const fn = ( value: ObservableReadonly<boolean> ): void => {
value (); // Getting
value ( true ); // This will throw!
};This type says that something can be the value itself or an observable to that value.
This type is particularly useful for creating components and hooks that can accept either plain values or observables, providing flexibility in API design.
Interface:
type ObservableMaybe<T> = Observable<T> | ObservableReadonly<T> | T;Usage:
import type {ObservableMaybe} from 'woby';
const Button = ({ label }: { label: ObservableMaybe<string> }): JSX.Element => {
return <button>{label}</button>;
};This type describes the options object that various functions can accept to tweak how the underlying observable works.
Interface:
type ObservableOptions<T> = {
equals?: (( value: T, valuePrev: T ) => boolean) | false,
type?: 'string' | 'function' | 'object' | 'number' | 'boolean' | 'symbol' | 'undefined' | 'bigint' | Constructor<any> | T,
toHtml?: (t: T) => string,
fromHtml?: (s: string) => T
};The type option provides runtime type checking for observables. When specified, any value assigned to the observable will be validated against this type, and a TypeError will be thrown if the types don't match.
The toHtml option provides a function to convert the observable value to a string representation for HTML attributes. This is useful when binding observables to DOM element attributes.
The fromHtml option provides a function to convert a string value from HTML attributes back to the observable's type. This is useful when binding HTML attributes back to observables.
The type option supports:
- Primitive type strings:
'string','number','boolean','function','object','symbol','undefined','bigint' - Constructor functions (like
String,Number,Boolean, etc.) - Custom constructor types
- Generic type
T
Usage:
import type {Observable, ObservableOptions} from 'woby';
import {$} from 'woby';
const createTimestamp = ( options?: ObservableOptions ): Observable<number> => {
return $( Date.now (), options );
};
// Create an observable that only accepts number values
const numberObservable = $( 0, { type: 'number' } );
// This would work fine
numberObservable( 123 );
// This would throw a TypeError at runtime
// numberObservable('123'); // TypeError: Expected value of type 'number', but received 'string'
// Create an observable with HTML conversion functions
const dateObservable = $(new Date(), {
type: 'object',
toHtml: (date) => date.toISOString(),
fromHtml: (str) => new Date(str)
});This type represents the return value of useResource, usePromise, and useFetch functions.
It represents the state of a resource, indicating whether it is loading, if an error occurred, and what the resulting value will be.
This read-only observable contains the resulting object and provides helper methods for accessing specific properties, enabling cleaner code patterns.
Helper methods are automatically memoized for optimal performance.
Interface:
type ResourceStaticPending<T> = { pending: true, error?: never, value?: never, latest?: T };
type ResourceStaticRejected = { pending: false, error: Error, value?: never, latest?: never };
type ResourceStaticResolved<T> = { pending: false, error?: never, value: T, latest: T };
type ResourceStatic<T> = ResourceStaticPending<T> | ResourceStaticRejected | ResourceStaticResolved<T>;
type ResourceFunction<T> = { pending (): boolean, error (): Error | undefined, value (): T | undefined, latest (): T | undefined };
type Resource<T> = ObservableReadonly<ResourceStatic<T>> & ResourceFunction<T>;Usage:
import type {ObservableReadonly, Resource} from 'woby';
const resource: Resource<Response> = useResource ( () => fetch ( 'https://my.api' ) );
// Reading the static object
resource ().pending; // => true | false
resource ().error; // => Error | undefined
resource ().value; // => Whatever the resource will resolve to
resource ().latest; // => Whatever the resource will resolve to, or the previous known resolved value if the resource is pending
// Using helper methods
resource.pending (); // => true | false
resource.error (); // => Error | undefined
resource.value (); // => Whatever the resource will resolve to
resource.latest (); // => Whatever the resource will resolve to, or the previous known resolved value if the resource is pendingThis type describes the options object that the store function accepts.
Interface:
type StoreOptions = {
unwrap?: boolean
};Usage:
import type {StoreOptions} from 'woby';
import {store} from 'woby';
const createStore = <T> ( value: T, options?: StoreOptions ): T => {
return store ( value, options );
};Extra features and details.
If you'd like to contribute to this repo you should take the following steps to install Woby locally:
git clone https://github.com/wobyjs/woby.git
cd woby
npm install
npm run compileThen you can run any of the demos locally like this:
# Playground
npm run dev
# Counter
npm run dev:counter
# Benchmark
npm run dev:benchmarkJSX is supported out of the box and is similar to React JSX with some key differences.
- Attributes can accept plain values, observables, or functions. When observables or functions are provided, attributes update in a fine-grained manner.
- There's no "key" attribute because it's unnecessary.
- Only function-form refs are supported, encouraging the use of observables for ref management.
- The "ref" attribute can also accept an array of functions to call, for convenience.
- The "class" attribute can be used instead of "className".
- The "class" attribute also accepts objects or arrays of classes for enhanced flexibility.
- SVGs are supported out of the box and will also be updated in a fine-grained manner.
- The "innerHTML", "outerHTML", and "textContent" attributes are restricted to prevent anti-patterns and encourage idiomatic usage.
- A React-like "dangerouslySetInnerHTML" attribute is supported for setting some raw HTML.
- Numbers set as values for style properties that require a unit to be provided will automatically be suffixed with "px".
- Using CSS variables in the "style" object is supported out of the box.
- The following events are delegated, automatically:
beforeinput,click,dblclick,focusin,focusout,input,keydown,keyup,mousedown,mouseup. - Events always bubble according to the natural DOM hierarchy, there's no special bubbling logic for
Portal. - Class components without lifecycle callbacks are also supported. While other frameworks have moved away from class components entirely, organizing internal methods within classes and automatically assigning them to refs provides a useful organizational pattern.
To use Woby with TypeScript, two main configurations are required:
- Since Woby is an ESM-only framework, mark your package as ESM by adding the following to your
package.json:"type": "module" - Configure TypeScript to load the correct JSX types by adding the following to your
tsconfig.json:{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "woby" } }
For a complete TypeScript configuration example, you can use the following tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "esnext",
"moduleResolution": "bundler",
"lib": [
"ES2020",
"DOM"
],
"allowJs": true,
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"jsxImportSource": "woby",
"allowSyntheticDefaultImports": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"noImplicitAny": false,
"strictNullChecks": false,
"baseUrl": "."
},
"include": [
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules",
"dist"
]
}Optionally, if not using a bundler or if a plugin is not available for your bundler, define a "React" variable in scope and use the React JSX transform:
import * as React from 'woby';- S: for pioneering reactive programming approaches that inspired this framework.
- sinuous/observable: for providing an excellent Observable implementation that served as the foundation for this library.
- solid: for serving as a reference implementation, popularizing signal-based reactivity, and building a strong community.
- trkl: for demonstrating the power of minimal, focused implementations.
MIT