UNPKG

tuix

Version:

A performant TUI framework for Bun with JSX and reactive state management

301 lines (261 loc) 6.73 kB
/** * Component Lifecycle Hooks * * Provides lifecycle management for components similar to Svelte */ import { Effect, Ref } from "effect" export interface LifecycleHook { (): void | Promise<void> | (() => void) } export interface AsyncLifecycleHook { (): Promise<void> | Promise<() => void> } // Global lifecycle context interface LifecycleContext { currentComponent: string | null hooks: Map<string, ComponentHooks> } interface ComponentHooks { mount: LifecycleHook[] destroy: LifecycleHook[] beforeUpdate: LifecycleHook[] afterUpdate: LifecycleHook[] cleanups: Array<() => void> } const lifecycleContext: LifecycleContext = { currentComponent: null, hooks: new Map() } let componentId = 0 /** * Get or create hooks for the current component */ function getCurrentHooks(): ComponentHooks { if (!lifecycleContext.currentComponent) { lifecycleContext.currentComponent = `component_${++componentId}` } const id = lifecycleContext.currentComponent if (!lifecycleContext.hooks.has(id)) { lifecycleContext.hooks.set(id, { mount: [], destroy: [], beforeUpdate: [], afterUpdate: [], cleanups: [] }) } return lifecycleContext.hooks.get(id)! } /** * Set the current component context */ export function setCurrentComponent(id: string) { lifecycleContext.currentComponent = id } /** * Clear the current component context */ export function clearCurrentComponent() { lifecycleContext.currentComponent = null } /** * Run when component is mounted */ export function onMount(fn: LifecycleHook) { const hooks = getCurrentHooks() hooks.mount.push(fn) } /** * Run when component is destroyed */ export function onDestroy(fn: LifecycleHook) { const hooks = getCurrentHooks() hooks.destroy.push(fn) } /** * Run before component updates */ export function beforeUpdate(fn: LifecycleHook) { const hooks = getCurrentHooks() hooks.beforeUpdate.push(fn) } /** * Run after component updates */ export function afterUpdate(fn: LifecycleHook) { const hooks = getCurrentHooks() hooks.afterUpdate.push(fn) } /** * Execute all mount hooks for a component */ export async function executeMountHooks(componentId: string) { const hooks = lifecycleContext.hooks.get(componentId) if (!hooks) return for (const hook of hooks.mount) { try { const result = hook() // If hook returns a cleanup function, store it if (typeof result === 'function') { hooks.cleanups.push(result) } else if (result instanceof Promise) { const cleanup = await result if (typeof cleanup === 'function') { hooks.cleanups.push(cleanup) } } } catch (error) { console.error(`Error in onMount hook:`, error) } } } /** * Execute all destroy hooks for a component */ export async function executeDestroyHooks(componentId: string) { const hooks = lifecycleContext.hooks.get(componentId) if (!hooks) return // Run cleanup functions first for (const cleanup of hooks.cleanups) { try { cleanup() } catch (error) { console.error(`Error in cleanup function:`, error) } } // Then run destroy hooks for (const hook of hooks.destroy) { try { const result = hook() if (result instanceof Promise) { await result } } catch (error) { console.error(`Error in onDestroy hook:`, error) } } // Clean up hooks map lifecycleContext.hooks.delete(componentId) } /** * Execute before update hooks */ export async function executeBeforeUpdateHooks(componentId: string) { const hooks = lifecycleContext.hooks.get(componentId) if (!hooks) return for (const hook of hooks.beforeUpdate) { try { const result = hook() if (result instanceof Promise) { await result } } catch (error) { console.error(`Error in beforeUpdate hook:`, error) } } } /** * Execute after update hooks */ export async function executeAfterUpdateHooks(componentId: string) { const hooks = lifecycleContext.hooks.get(componentId) if (!hooks) return for (const hook of hooks.afterUpdate) { try { const result = hook() if (result instanceof Promise) { await result } } catch (error) { console.error(`Error in afterUpdate hook:`, error) } } } /** * Utility to automatically manage component lifecycle */ export function createLifecycleManager(componentId: string) { return { async mount() { await executeMountHooks(componentId) }, async destroy() { await executeDestroyHooks(componentId) }, async beforeUpdate() { await executeBeforeUpdateHooks(componentId) }, async afterUpdate() { await executeAfterUpdateHooks(componentId) } } } /** * Tick - schedule a callback to run after the next update */ export function tick(): Promise<void> { return new Promise(resolve => { // In a real implementation, this would integrate with the render cycle setTimeout(resolve, 0) }) } /** * Create a custom hook that can use other hooks */ export function createHook<T extends any[], R>( fn: (...args: T) => R ): (...args: T) => R { return (...args: T) => { // Ensure we're in a component context if (!lifecycleContext.currentComponent) { throw new Error("Hooks can only be called during component initialization") } return fn(...args) } } /** * Use interval - runs a function at regular intervals */ export const useInterval = createHook((callback: () => void, delay: number) => { onMount(() => { const interval = setInterval(callback, delay) return () => clearInterval(interval) }) }) /** * Use timeout - runs a function after a delay */ export const useTimeout = createHook((callback: () => void, delay: number) => { onMount(() => { const timeout = setTimeout(callback, delay) return () => clearTimeout(timeout) }) }) /** * Use async effect - for handling async side effects */ export const useAsyncEffect = createHook(( effect: () => Promise<void | (() => void)>, dependencies?: any[] ) => { onMount(async () => { try { const cleanup = await effect() if (typeof cleanup === 'function') { return cleanup } } catch (error) { console.error("Error in async effect:", error) } }) }) /** * Use previous value - keeps track of the previous value of a variable */ export const usePrevious = createHook(<T>(value: T): T | undefined => { const ref = Ref.make<T | undefined>(undefined) afterUpdate(() => { Effect.runSync(Ref.set(ref, value)) }) return Effect.runSync(Ref.get(ref)) })