woby
Version:
A high-performance framework with fine-grained observable/signal-based reactivity for building rich applications.
450 lines (331 loc) • 11.6 kB
Markdown
This document covers the essential methods and functions provided by Woby for building reactive applications.
- [Observable Creation ($)](#observable-creation-)
- [Observable Unwrapping ($$)](#observable-unwrapping-)
- [Effect Management](#effect-management)
- [Memoization](#memoization)
- [Batching](#batching)
- [Untracking](#untracking)
- [Component Rendering](#component-rendering)
- [Context Creation (createContext)](#context-creation-createcontext)
- [Custom Elements (customElement)](#custom-elements-customelement)
- [Object Assignment (assign)](#object-assignment-assign)
- [Component Defaults (defaults)](#component-defaults-defaults)
## Observable Creation ($)
The `$()` function creates reactive observables that can track dependencies and notify subscribers of changes.
```typescript
import { $ } from 'woby'
// Create a primitive observable
const count = $(0)
// Create a computed observable
const doubled = $(() => $(count) * 2)
// Create an observable with custom equality function
const user = $({ id: 1, name: 'John' }, {
equals: (a, b) => a.id === b.id
})
```
Observables can be created with options to control their behavior, including type information for custom elements:
```typescript
// Typed observables for proper HTML attribute synchronization
const count = $(0, { type: 'number' } as const)
const enabled = $(true, { type: 'boolean' } as const)
const data = $({} as any, { type: 'object' } as const)
// Custom equality function
const user = $({ id: 1, name: 'John' }, {
equals: (a, b) => a.id === b.id
})
```
For detailed information about type synchronization between HTML attributes and component props, see the [Type Synchronization Documentation](./Type-Synchronization.md) or the [Simple Type Synchronization Guide](./Type-Sync-Simple.md) for a more straightforward approach.
The `$$()` function (alias for Soby's `$.get`) safely unwraps observables, providing a consistent way to access values whether they're observables or plain values.
```typescript
import { $, $$ } from 'woby'
// Unwrap a primitive observable
const count = $(0)
console.log($$(count)) // 0
// Unwrap a computed observable
const doubled = $(() => $$(count) * 2)
console.log($$(doubled)) // 0
// Safely unwrap uncertain values
const maybeObservable = getValue() // Could be observable or plain value
const value = $$(maybeObservable) // Works for both cases
```
Recent enhancements to Soby (which Woby uses as its reactive core) 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. This provides seamless integration in various contexts:
#### Technical Implementation
The enhancement was implemented in Soby's `src/objects/callable.ts` by adding the following lines to both `readable` and `writable` observable function generators:
```typescript
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:
```typescript
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:
```typescript
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 values
```
For more detailed information about working with observables and the `$$` function, see the [Reactive Utilities Documentation](./Reactive-Utilities.md).
Effects automatically track dependencies and re-run when those dependencies change.
```typescript
import { $, $$, useEffect } from 'woby'
const count = $(0)
const name = $('John')
// Effect automatically tracks count and name
useEffect(() => {
console.log(`Count: ${$$count)}, Name: ${$$name)}`)
})
// Effect re-runs when either observable changes
count(1) // Logs: "Count: 1, Name: John"
name('Jane') // Logs: "Count: 1, Name: Jane"
```
Memoized computations automatically recompute when their dependencies change.
```typescript
import { $, $$, useMemo } from 'woby'
const firstName = $('John')
const lastName = $('Doe')
// Computed observable that updates when dependencies change
const fullName = useMemo(() => {
return `${$$firstName)} ${$$lastName)}`
})
console.log($$(fullName)) // "John Doe"
firstName('Jane')
console.log($$(fullName)) // "Jane Doe"
```
Batch multiple updates to trigger only a single re-render.
```typescript
import { $, $$, batch } from 'woby'
const count = $(0)
const name = $('John')
// Without batching - triggers two updates
count(1)
name('Jane')
// With batching - triggers one update
batch(() => {
count(2)
name('Bob')
})
```
Temporarily read observables without creating dependencies.
```typescript
import { $, $$, untrack } from 'woby'
const count = $(0)
const other = $(0)
const computed = $(() => {
const c = $$(count) // Creates a dependency
const o = untrack(() => $$(other)) // Does not create a dependency
return c + o
})
count(1) // Triggers update
other(1) // Does not trigger update
```
Render components to the DOM.
```typescript
import { $, render } from 'woby'
const App = () => {
const count = $(0)
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => count(c => $$(c) + 1)}>+</button>
</div>
)
}
render(<App />, document.getElementById('app'))
```
**Reactive Content Patterns:**
In Woby, there are several ways to create reactive content in your components:
1. **Direct Observable Passing (Recommended for simple cases):**
```typescript
const Component = () => {
const userName = $('John')
// ✅ Direct observable - automatically reactive and preferred for single child elements
return <div>{userName}</div>
}
```
2. **Function Expressions (For complex expressions):**
```typescript
const Component = () => {
const userName = $('John')
const show = $(true)
// ✅ Function expression - automatically reactive
return <div>Hello {() => $$(userName)}</div>
// ✅ Complex expressions with multiple observables
return <div class={['w-full', () => $$(show) ? '' : 'hidden']}>Content</div>
}
```
3. **Memoized Expressions (When needed for expensive computations):**
```typescript
const Component = () => {
const userName = $('John')
// ✅ Memoized expression - reactive but often unnecessary
return <div>Hello {useMemo(() => $$(userName))}</div>
// Note: For simple expressions like this, useMemo is unnecessary
// since () => $$(userName) is automatically tracked
}
```
**Common Mistake - Non-Reactive Content:**
```typescript
const Component = () => {
const userName = $('John')
// ❌ Not reactive - only executes once
return <div>Hello {$$(userName)}</div>
}
```
In the non-reactive example above, `$$()` is called during component creation (which only happens once), so the content never updates even when `userName` changes.
**Reactive Attributes:**
For attributes, you can directly pass observables for automatic reactivity:
```typescript
const Component = () => {
const valid = $(true)
const show = $(true)
// ✅ Direct observable - automatically reactive
return <input disabled={valid} />
// ✅ Function expression - reactive
return <input disabled={() => $$(valid) ? true : undefined} />
// ✅ Complex reactive class expressions
return <div class={['w-full', () => $$(show) ? '' : 'hidden']}>Content</div>
}
```
**Key Points:**
- Direct observable passing (`{userName}`) is the preferred pattern for simple cases with only one child
- Function expressions (`{() => $$(userName)}`) are automatically tracked and suitable for complex expressions
- `useMemo` is unnecessary for simple expressions since `() =>` is automatically tracked
- Avoid `{$$()}` patterns as they only execute once and are not reactive
Create reactive stores for complex nested state.
```typescript
import { store, $$ } from 'woby'
const userStore = store({
personal: {
name: 'John',
age: 30
},
preferences: {
theme: 'dark'
}
})
// Access nested properties
console.log($$(userStore.personal.name)) // 'John'
console.log($$(userStore.preferences.theme)) // 'dark'
// Update nested properties
userStore.personal.name('Jane')
userStore.preferences.theme('light')
```
Use built-in components for conditional rendering.
```typescript
import { $, If, Switch, Match } from 'woby'
const showContent = $(true)
const status = $('loading')
const Component = () => (
<div>
<If when={showContent}>
<p>This content is conditionally shown</p>
</If>
<Switch>
<Match when={() => $$(status) === 'loading'}>
<p>Loading...</p>
</Match>
<Match when={() => $$(status) === 'success'}>
<p>Success!</p>
</Match>
<Match when={() => $$(status) === 'error'}>
<p>Error occurred</p>
</Match>
</Switch>
</div>
)
```
Efficiently render lists with the `For` component.
```typescript
import { $, For } from 'woby'
const items = $([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
])
const Component = () => (
<ul>
<For values={items}>
{(item) => (
<li key={item.id}>{item.name}</li>
)}
</For>
</ul>
)
```
Handle DOM events with reactive callbacks.
```typescript
import { $, $$ } from 'woby'
const Component = () => {
const count = $(0)
const handleClick = () => {
count(c => $$(c) + 1)
}
const handleInputChange = (e) => {
count(parseInt(e.target.value) || 0)
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+</button>
<input
type="number"
value={count}
onInput={handleInputChange}
/>
</div>
)
}
```
Handle asynchronous data with resource utilities.
```typescript
import { useResource, $$ } from 'woby'
const DataComponent = () => {
const data = useResource(async () => {
const response = await fetch('/api/data')
return response.json()
})
return (
<div>
<If when={() => $$(data.loading)}>
<p>Loading...</p>
</If>
<If when={() => $$(data.error)}>
<p>Error: {$$(data.error).message}</p>
</If>
<If when={data}>
<pre>{JSON.stringify($$(data), null, 2)}</pre>
</If>
</div>
)
}
```