UNPKG

woby

Version:

A high-performance framework with fine-grained observable/signal-based reactivity for building rich applications.

818 lines (620 loc) 16.9 kB
# Hooks Woby provides a comprehensive set of hooks for common patterns and utilities. All hooks are just regular functions that can be called conditionally, nested, or used outside components. ## Table of Contents - [State Hooks](#state-hooks) - [Effect Hooks](#effect-hooks) - [Resource Hooks](#resource-hooks) - [Timer Hooks](#timer-hooks) - [Utility Hooks](#utility-hooks) - [DOM Hooks](#dom-hooks) - [Context Hooks](#context-hooks) ## State Hooks ### useMemo Creates a memoized computed value that only updates when dependencies change. **Signature:** ```typescript function useMemo<T>(fn: () => T): Observable<T> ``` **Usage:** ```typescript import { $, useMemo } from 'woby' const Component = () => { const count = $(0) const doubled = useMemo(() => $$(count) * 2) return ( <div> <p>Count: {count}</p> <p>Doubled: {doubled}</p> <button onClick={() => count(c => c + 1)}>+</button> </div> ) } ``` **Best Practices:** In Woby, event handlers are automatically optimized by the framework's dependency tracking system, eliminating the need for manual memoization: ```typescript // ❌ Over-engineered - Attempting to replicate React patterns const handleClick = useMemo(() => { return (e) => { console.log('Button clicked') } }) // ✅ Clean and efficient - Woby's automatic tracking handles optimization const handleClick = (e) => { console.log('Button clicked') } ``` ### useBoolean Provides utilities for boolean state management. **Signature:** ```typescript function useBoolean(initial?: boolean): [ Observable<boolean>, () => void, // toggle () => void, // setTrue () => void // setFalse ] ``` **Usage:** ```typescript import { useBoolean } from 'woby' const Component = () => { const [isVisible, toggle, show, hide] = useBoolean(false) return ( <div> <button onClick={toggle}>Toggle</button> <button onClick={show}>Show</button> <button onClick={hide}>Hide</button> {() =< $$(isVisible) && <p>I'm visible!</p>} </div> ) } ``` ### useReadonly Creates a readonly version of an observable. **Signature:** ```typescript function useReadonly<T>(observable: Observable<T>): ObservableReadonly<T> ``` **Usage:** ```typescript import { $, useReadonly } from 'woby' const Component = () => { const count = $(0) const readonlyCount = useReadonly(count) // readonlyCount() works // readonlyCount(5) // TypeScript error! return <div>Count: {readonlyCount}</div> } ``` ## Effect Hooks ### useEffect Runs side effects in response to observable changes. **Signature:** ```typescript function useEffect(fn: () => void | (() => void)): void ``` **Usage:** ```typescript import { $, useEffect } from 'woby' const Component = () => { const count = $(0) useEffect(() => { console.log('Count changed:', $$(count)) // Optional cleanup function return () => { console.log('Cleaning up effect') } }) return ( <div> <p>Count: {count}</p> <button onClick={() => count(c => c + 1)}>+</button> </div> ) } ``` React behavior (for comparison): ```typescript import { useEffect } from 'react' function Component() { useEffect(() => { console.log('Component mounted or updated') return () => { console.log('Component will unmount') } }, []) // Dependency array controls when effect runs return <div>Hello World</div> } ``` Woby behavior: ```typescript import { useEffect } from 'woby' function Component() { useEffect(() => { console.log('Component mounted or updated') return () => { console.log('Component will unmount') } }) // No dependency array needed return <div>Hello World</div> } ``` **Automatic Dependency Tracking:** Unlike React's useEffect which requires a dependency array, Woby's useEffect automatically tracks which observables are accessed within its callback: ```typescript import { $, $$, useEffect } from 'woby' const userId = $(123) const theme = $('dark') // Woby automatically tracks userId and theme useEffect(() => { console.log(`User: ${$$(userId)}, Theme: ${$$(theme)}`) // This effect will re-run whenever userId or theme changes }) // No dependency array needed - Woby handles tracking automatically ``` **Reactive Content in Components:** In Woby, components execute once but reactive content updates automatically. There are several patterns for creating reactive content: ```typescript const ReactiveContentComponent = () => { const userName = $('John') const show = $(true) const valid = $(true) // ✅ Direct observable passing - simplest and most common (only 1 child) return <div>{userName}</div> // ✅ Function expressions - for more complex reactive expressions return <div>Hello {() => $$(userName)}</div> // ✅ Memoized expressions - for expensive computations (often unnecessary) return <div>Hello {useMemo(() => $$(userName))}</div> // ✅ Reactive attributes return <input disabled={valid} /> // Direct observable - reactive // ✅ Complex reactive class expressions return <div class={['w-full', () => $$(show) ? '' : 'hidden']}>Content</div> } ``` **Important Notes:** - Components execute once on mount, not on every render like React - Reactive content updates automatically when observables change - Direct observable passing (`{userName}`) is preferred for simple cases with only one child - Function expressions (`{() => $$(userName)}`) are automatically tracked - `useMemo` is unnecessary for simple expressions since `() =>` is automatically tracked - Avoid `{$$()}` patterns as they only execute once and are not reactive **Effect Organization:** Separate unrelated concerns into individual effects for better performance: ```typescript import { $, $$, useEffect } from 'woby' const name = $('John') const count = $(0) // ✅ Recommended - Separate effects for different concerns useEffect(() => { console.log('Name changed:', $$(name)) }) useEffect(() => { console.log('Count changed:', $$(count)) }) // ❌ Not recommended - Single effect with unrelated dependencies // useEffect(() => { // console.log('Name:', $$(name)) // console.log('Count:', $$(count)) // }) ``` **Optimization with Early Returns:** Use early returns to skip unnecessary work: ```typescript import { $, $$, useEffect } from 'woby' const name = $('John') const previousName = $('John') useEffect(() => { // Skip if name hasn't changed meaningfully if ($$(previousName) === $$(name)) return previousName($$(name)) performExpensiveOperation($$(name)) }) ``` ### useReaction Creates a reaction that runs when dependencies change. **Signature:** ```typescript function useReaction<T>( tracking: () => T, reaction: (value: T, prev: T) => void ): void ``` **Usage:** ```typescript import { $, useReaction } from 'woby' const Component = () => { const name = $('John') useReaction( () => $$(name), (newName, oldName) => { console.log(`Name changed from ${oldName} to ${newName}`) } ) return ( <input value={name} onInput={(e) => name(e.target.value)} /> ) } ``` ### useCleanup Registers a cleanup function to run when the component unmounts. **Signature:** ```typescript function useCleanup(fn: () => void): void ``` **Usage:** ```typescript import { useCleanup } from 'woby' const Component = () => { const interval = setInterval(() => { console.log('tick') }, 1000) useCleanup(() => { clearInterval(interval) }) return <div>Check console for ticks</div> } ``` ## Resource Hooks ### useResource Creates a resource that can be loaded asynchronously. **Signature:** ```typescript function useResource<T>( fetcher: () => Promise<T>, initialValue?: T ): Resource<T> ``` **Usage:** ```typescript import { $, useResource } from 'woby' const UserProfile = () => { const userId = $(1) const user = useResource(async () => { const response = await fetch(`/api/users/${$$(userId)}`) return response.json() }) return ( <div> {$$(user.loading) && <p>Loading...</p>} {$$(user.error) && <p>Error: {$$(user.error).message}</p>} {$$(user) && <p>Hello, {$$(user).name}!</p>} </div> ) } ``` ### useFetch Simplified fetch hook for common HTTP requests. **Signature:** ```typescript function useFetch<T>( url: ObservableMaybe<string>, options?: RequestInit ): Resource<T> ``` **Usage:** ```typescript import { $, useFetch } from 'woby' const PostList = () => { const posts = useFetch<Post[]>('/api/posts') return ( <div> {$$(posts.loading) && <div>Loading posts...</div>} {$$(posts.error) && <div>Failed to load posts</div>} {$$(posts) && ( <ul> {$$(posts).map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> )} </div> ) } ``` ### usePromise Wraps a promise in a resource-like interface. **Signature:** ```typescript function usePromise<T>( promise: ObservableMaybe<Promise<T>> ): Resource<T> ``` **Usage:** ```typescript import { $, usePromise } from 'woby' const Component = () => { const promise = $<Promise<string>>() const result = usePromise(promise) const loadData = () => { promise(fetch('/api/data').then(r => r.text())) } return ( <div> <button onClick={loadData}>Load Data</button> {$$(result.loading) && <p>Loading...</p>} {$$(result) && <p>Data: {$$(result)}</p>} </div> ) } ``` ## Timer Hooks ### useTimeout Executes a function after a specified delay. **Signature:** ```typescript function useTimeout( fn: () => void, delay: ObservableMaybe<number> ): void ``` **Usage:** ```typescript import { $, useTimeout } from 'woby' const Component = () => { const message = $('') useTimeout(() => { message('Hello after 2 seconds!') }, 2000) return <div>{message}</div> } ``` ### useInterval Executes a function repeatedly at specified intervals. **Signature:** ```typescript function useInterval( fn: () => void, delay: ObservableMaybe<number> ): void ``` **Usage:** ```typescript import { $, useInterval } from 'woby' const Clock = () => { const time = $(new Date()) useInterval(() => { time(new Date()) }, 1000) return <div>Current time: {() => $$(time).toLocaleTimeString()}</div> } ``` ### useAnimationFrame Executes a function on the next animation frame. **Signature:** ```typescript function useAnimationFrame(fn: () => void): void ``` **Usage:** ```typescript import { $, useAnimationFrame } from 'woby' const AnimatedComponent = () => { const position = $(0) useAnimationFrame(() => { position(p => $$(p) + 1) }) return ( <div style={{ transform: `translateX(${$$(position)}px)` }}> Moving element </div> ) } ``` ### useAnimationLoop Creates a continuous animation loop. **Signature:** ```typescript function useAnimationLoop(fn: (time: number) => void): void ``` **Usage:** ```typescript import { $, useAnimationLoop } from 'woby' const AnimationDemo = () => { const rotation = $(0) useAnimationLoop((time) => { rotation(time * 0.001) // Convert to seconds }) return ( <div style={{ transform: `rotate(${$$(rotation)}rad)`, transition: 'none' }}> 🎨 </div> ) } ``` ## Utility Hooks ### useContext Consumes a context value in JSX/TSX components only. **Signature:** ```typescript function useContext<T>(context: Context<T>): T ``` **Usage:** ```typescript import { createContext, useContext } from 'woby' const ThemeContext = createContext<'light' | 'dark'>('light') const ThemedComponent = () => { const theme = useContext(ThemeContext) return ( <div class={`theme-${$$(theme)}`}> Current theme: {theme} </div> ) } ``` **Limitations:** - Only works in JSX/TSX components - Requires explicit Provider components - Does not work with custom elements defined directly in HTML ### useDisposed Checks if the current component has been disposed. **Signature:** ```typescript function useDisposed(): Observable<boolean> ``` **Usage:** ```typescript import { useDisposed, useEffect } from 'woby' const Component = () => { const disposed = useDisposed() useEffect(() => { const timer = setInterval(() => { if (!$$(disposed)) { console.log('Component is still alive') } }, 1000) return () => clearInterval(timer) }) return <div>Check console</div> } ``` ### useRoot Creates a new reactive root for isolation. **Signature:** ```typescript function useRoot<T>(fn: () => T): T ``` **Usage:** ```typescript import { $, useRoot } from 'woby' const Component = () => { const result = useRoot(() => { const count = $(0) const doubled = $(() => $$(count) * 2) return { count, doubled } }) return ( <div> <p>Count: {result.count}</p> <p>Doubled: {result.doubled}</p> </div> ) } ``` ## DOM Hooks ### useEventListener Adds event listeners to DOM elements or the window. **Signature:** ```typescript function useEventListener<T extends keyof WindowEventMap>( target: EventTarget | (() => EventTarget), type: T, handler: (event: WindowEventMap[T]) => void, options?: AddEventListenerOptions ): void ``` **Usage:** ```typescript import { $, useEventListener } from 'woby' const Component = () => { const count = $(0) useEventListener(window, 'keydown', (e) => { if (e.key === ' ') { count(c => $$(c) + 1) } }) return ( <div> <p>Count: {() => $$(count)}</p> <p>Press spacebar to increment</p> </div> ) } ``` ### useIdleCallback Executes a function during idle time. **Signature:** ```typescript function useIdleCallback( fn: () => void, options?: IdleRequestOptions ): void ``` **Usage:** ```typescript import { useIdleCallback } from 'woby' const Component = () => { useIdleCallback(() => { console.log('Browser is idle, doing background work') }) return <div>Component with background processing</div> } ``` ### useMicrotask Executes a function in the next microtask. **Signature:** ```typescript function useMicrotask(fn: () => void): void ``` **Usage:** ```typescript import { $, useMicrotask } from 'woby' const Component = () => { const message = $('') useMicrotask(() => { message('Updated in microtask') }) return <div>{message}</div> } ``` ## Advanced Patterns ### Conditional Hooks Since hooks are just functions, you can call them conditionally: ```typescript const Component = ({ useSpecialFeature }: { useSpecialFeature: boolean }) => { const count = $(0) if (useSpecialFeature) { useInterval(() => { count(c => $$(c) + 1) }, 1000) } return <div>Count: {count}</div> } ``` ### Custom Hooks Create your own hooks by combining existing ones: ```typescript function useCounter(initial = 0) { const count = $(initial) const increment = () => count(c => c + 1) const decrement = () => count(c => c - 1) const reset = () => count(initial) return { count, increment, decrement, reset } } const CounterComponent = () => { const { count, increment, decrement, reset } = useCounter(10) return ( <div> <p>Count: {count}</p> <button onClick={increment}>+</button> <button onClick={decrement}>-</button> <button onClick={reset}>Reset</button> </div> ) } ``` ### Nested Hooks Hooks can be nested and called from anywhere: ```typescript function useAuth() { const user = $<User | null>(null) const login = useCallback(async (credentials) => { const userData = await api.login(credentials) user(userData) }) return { user, login } } function usePermissions() { const { user } = useAuth() const permissions = useMemo(() => { return $$(user)?.permissions || [] }) return { permissions } } ``` For more advanced patterns and examples, see our [Best Practices](./Best-Practices.md) guide.