# rEFui - Retained Mode JavaScript Framework (v0.8.0) rEFui (pronounced "refuel") is a lightweight retained-mode JavaScript framework for building reactive user interfaces across web, native, and embedded platforms. Built on a fine-grained reactive signal system, rEFui provides automatic dependency tracking, efficient updates, and built-in Hot Module Replacement support. The framework uses a component-based architecture where components are pure functions that return render functions, enabling predictable state management and clean composition patterns. At its core, rEFui implements a reactive signal system that automatically tracks dependencies and batches updates for optimal performance. Components receive props and return render functions that can access reactive signals, with lifecycle hooks managed through an automatic disposal system. The framework supports multiple rendering targets through a pluggable renderer architecture, including DOM manipulation for browsers, HTML string generation for server-side rendering, and custom renderers for native and embedded platforms. With zero dependencies and a small footprint, rEFui is designed for projects that need reactive UI capabilities without the overhead of larger frameworks. As of v0.8.0, the component API has been refined with the `expose` prop pattern replacing the previous global `expose` helper, providing better context handling for async components. ## Installation ```bash npm i refui ``` ## Creating Reactive Signals ```javascript import { signal, computed, watch, nextTick } from 'refui' // Basic signal const count = signal(0) console.log(count.value) // 0 count.value = 5 console.log(count.value) // 5 // Computed signal (derived) const doubled = computed(() => count.value * 2) console.log(doubled.value) // 10 // Effects are lazily evaluated - use nextTick to see updates count.value = 7 nextTick(() => { console.log(doubled.value) // 14 }) // Watch for changes const dispose = watch(() => { console.log('Count changed:', count.value) }) count.value = 10 // Logs: "Count changed: 10" // Clean up dispose() ``` ## Signal Operations and Transformations ```javascript import { signal, nextTick } from 'refui' const count = signal(5) const enabled = signal(true) // Logical operations const isPositive = count.gt(0) const isNegative = count.lt(0) const isNonNegative = count.gte(0) const isNonPositive = count.lte(0) const isZero = count.eq(0) const isNotZero = count.neq(0) // Combined operations const isValid = isPositive.and(enabled) // count > 0 && enabled const hasValueOrDefault = count.or(100) // count || 100 // Negation const disabled = enabled.inverse() // !enabled // Conditional selection const isDarkMode = signal(false) const theme = isDarkMode.choose('dark.css', 'light.css') console.log(theme.value) // 'light.css' isDarkMode.value = true nextTick(() => { console.log(theme.value) // 'dark.css' }) // Select from options const status = signal('idle') const messages = { idle: 'Waiting…', success: 'All good!', error: 'Something went wrong!' } const statusMessage = status.select(messages) console.log(statusMessage.value) // 'Waiting…' status.value = 'success' nextTick(() => { console.log(statusMessage.value) // 'All good!' }) // Nullish coalescing const username = signal(null) const displayName = username.nullishThen('Anonymous') console.log(displayName.value) // 'Anonymous' ``` ## Template Strings and Merging Signals ```javascript import { signal, tpl, merge, nextTick } from 'refui' // Template string signal const name = signal('Alice') const count = signal(3) const message = tpl`Hello ${name}, you have ${count} items` console.log(message.value) // "Hello Alice, you have 3 items" name.value = 'Bob' count.value = 5 nextTick(() => { console.log(message.value) // "Hello Bob, you have 5 items" }) // Merge multiple signals const firstName = signal('John') const lastName = signal('Doe') const fullName = merge([firstName, lastName], (first, last) => `${first} ${last}`) nextTick(() => { console.log(fullName.value) // "John Doe" }) firstName.value = 'Jane' nextTick(() => { console.log(fullName.value) // "Jane Doe" }) ``` ## DOM Rendering with JSX ```javascript import { signal } from 'refui' import { createDOMRenderer } from 'refui/dom' import { defaults } from 'refui/browser' const DOMRenderer = createDOMRenderer(defaults) const App = () => { const count = signal(0) const increment = () => { count.value += 1 } return (R) => ( <>

Hello, rEFui

Double: {count.value * 2}

) } DOMRenderer.render(document.body, App) ``` ## Component Props and Children ```javascript import { signal } from 'refui' import { createDOMRenderer } from 'refui/dom' import { defaults } from 'refui/browser' const DOMRenderer = createDOMRenderer(defaults) const Button = ({ label, variant = 'primary' }, ...children) => { const handleClick = () => { console.log('Button clicked:', label) } return (R) => ( ) } const App = () => (R) => (
) DOMRenderer.render(document.getElementById('app'), App) ``` ## Component References with $ref and expose ```javascript import { signal } from 'refui' // Component exposes API through the expose prop callback const Counter = ({ expose }) => { const count = signal(0) const increment = () => count.value++ const decrement = () => count.value-- const reset = () => count.value = 0 // Call expose prop to provide API to parent expose?.({ count, increment, decrement, reset }) return (R) => (

Count: {count}

) } const App = () => { const counterApi = signal() const inputRef = signal() const resetCounter = () => { counterApi.value?.reset() } const addTen = () => { for (let i = 0; i < 10; i++) { counterApi.value?.increment() } } const focusInput = () => { inputRef.value?.focus() } return (R) => (
{ counterApi.value = api }} />
) } ``` ## Conditional Rendering with If Component ```javascript import { signal, If } from 'refui' const LoginStatus = () => { const isLoggedIn = signal(false) const username = signal('Guest') const toggleLogin = () => { isLoggedIn.value = !isLoggedIn.value username.value = isLoggedIn.value ? 'Alice' : 'Guest' } return (R) => (
{(R) =>

Welcome back, {username}!

} {(R) =>

Please log in

}
) } // Alternative syntax with else prop const StatusBadge = () => { const isOnline = signal(true) return (R) => ( Offline}> {(R) => Online} ) } ``` ## List Rendering with For Component ```javascript import { signal, For } from 'refui' const TodoList = () => { const todos = signal([ { id: 1, text: 'Learn rEFui', done: false }, { id: 2, text: 'Build app', done: false }, { id: 3, text: 'Deploy', done: false } ]) const addTodo = () => { const newTodo = { id: Date.now(), text: `Task ${todos.value.length + 1}`, done: false } todos.value = [...todos.value, newTodo] } const toggleTodo = (id) => { todos.value = todos.value.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo ) } return (R) => (

Todo List

) } ``` ## Dynamic Components ```javascript import { signal, Dynamic } from 'refui' const HomeView = () => (R) =>

Home

Welcome!

const AboutView = () => (R) =>

About

About us

const ContactView = () => (R) =>

Contact

Get in touch

const Router = () => { const currentRoute = signal('home') const routes = { home: HomeView, about: AboutView, contact: ContactView } const component = signal(null) currentRoute.connect(() => { component.value = routes[currentRoute.value] || HomeView }) return (R) => (
) } ``` ## Async Components with Loading States ```javascript import { Async, signal } from 'refui' const fetchUser = async (id) => { const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`) if (!response.ok) throw new Error('Failed to fetch user') return response.json() } const UserProfile = ({ userId }) => { const user = fetchUser(userId) const LoadingView = () => (R) => (
Loading user...
) const ErrorView = ({ error }) => (R) => (
Error: {error.message}
) return (R) => ( {({ result }) => (R) => (

{result.name}

Email: {result.email}

Phone: {result.phone}

)}
) } const App = () => { const selectedUserId = signal(1) return (R) => (
) } ``` ## Lazy Loading Components ```javascript import { lazy, signal } from 'refui' // Lazy load a component const HeavyChart = lazy(() => import('./components/Chart.js'), 'default') const Dashboard = () => { const showChart = signal(false) return (R) => (

Dashboard

{showChart.value && ( )}
) } ``` ## Memoized Components ```javascript import { memo, useMemo, signal } from 'refui' // memo() caches the component instance creation const ExpensiveComponent = memo(({ data }) => { console.log('ExpensiveComponent rendered') return (R) => (

Expensive Calculation

Result: {data.reduce((a, b) => a + b, 0)}

) }) // useMemo() caches the render function creation const OptimizedList = useMemo(({ items }) => { console.log('OptimizedList render function created') return () => (R) => ( ) }) const App = () => { const data = signal([1, 2, 3, 4, 5]) const counter = signal(0) return (R) => (
({ id: i, text: `Item ${n}` }))} />
) } ``` ## Effects and Lifecycle Management ```javascript import { signal, watch, useEffect, onDispose, untrack } from 'refui' const Timer = () => { const seconds = signal(0) const isRunning = signal(true) // Auto-cleanup effect useEffect(() => { const interval = setInterval(() => { seconds.value++ }, 1000) // Cleanup function return () => { console.log('Clearing interval') clearInterval(interval) } }) // Manual effect with dispose const logEffect = watch(() => { if (isRunning.value) { console.log('Timer:', seconds.value) } }) // Cleanup on component disposal onDispose(() => { console.log('Timer component disposed') logEffect() }) // Untracked read (doesn't create dependency) const toggle = () => { const current = untrack(() => isRunning.value) isRunning.value = !current } return (R) => (

Timer: {seconds}s

) } ``` ## Async Components with expose Callback ```javascript import { signal, Async } from 'refui' // Async component that fetches data and exposes state to parent const AsyncDataLoader = async ({ url, expose }) => { try { const response = await fetch(url) if (!response.ok) throw new Error(`HTTP ${response.status}`) const data = await response.json() // In v0.8.0+, expose callback is already context-aware // No need to use capture() anymore expose?.({ data, error: null, loading: false }) return (R) => (

Data Loaded

{JSON.stringify(data, null, 2)}
) } catch (error) { expose?.({ data: null, error, loading: false }) return (R) => (

Error

{error.message}

) } } const App = () => { const dataState = signal({ loading: true }) const handleExpose = (payload) => { dataState.value = payload } return (R) => (

Async Data Loader

{({ result }) => result}

Loading: {dataState.value.loading ? 'Yes' : 'No'}

) } ``` ## Context Capture for Custom Functions ```javascript import { capture, getCurrentSelf } from 'refui' // capture() is still useful for preserving context in custom helper functions const DelayedAction = () => { const status = signal('idle') const performAction = () => { status.value = 'working' // Capture context for setTimeout callback const capturedSetStatus = capture((newStatus) => { status.value = newStatus }) setTimeout(() => { capturedSetStatus('done') }, 2000) } return (R) => (

Status: {status}

) } ``` ## Signal Utilities and Extraction ```javascript import { signal, derive, extract, derivedExtract, read, peek, write } from 'refui' const user = signal({ name: 'Alice', email: 'alice@example.com', age: 30, settings: { theme: 'dark' } }) // Derive a single property const userName = derive(user, 'name') console.log(userName.value) // 'Alice' // Extract multiple properties const { email, age } = extract(user, 'email', 'age') console.log(email.value) // 'alice@example.com' console.log(age.value) // 30 // Derived extraction (nested signals) const userWithSignals = signal({ name: signal('Bob'), status: signal('online') }) const { name, status } = derivedExtract(userWithSignals, 'name', 'status') // Read utilities (safe for signals or values) const maybeSignal = signal(42) const maybeValue = 100 console.log(read(maybeSignal)) // 42 console.log(read(maybeValue)) // 100 // Peek without creating dependencies const count = signal(5) console.log(peek(count)) // 5 (no dependency) // Write utility write(count, 10) // Sets count to 10 write(count, prev => prev + 5) // Sets count to 15 // Poke (set without triggering) const silent = signal(0) silent.poke(100) // Value changes but effects don't run silent.trigger() // Now effects run with value 100 ``` ## Conditional Matching with onCondition ```javascript import { signal, onCondition, nextTick } from 'refui' const App = () => { const state = signal('idle') const match = onCondition(state) const isIdle = match('idle') const isLoading = match('loading') const isSuccess = match('success') const isError = match('error') const startLoading = () => { state.value = 'loading' setTimeout(() => { state.value = Math.random() > 0.5 ? 'success' : 'error' }, 2000) } return (R) => (

Status: {state}

{(R) => } {(R) =>

Loading...

}
{(R) => (

Success!

)}
{(R) => (

Error occurred

)}
) } ``` ## Event Actions with useAction ```javascript import { useAction, signal } from 'refui' const NotificationSystem = () => { const [onNotify, triggerNotify] = useAction('idle') const notifications = signal([]) // Listen for notification actions onNotify((type) => { const id = Date.now() notifications.value = [ ...notifications.value, { id, type, message: `${type} notification` } ] // Auto-remove after 3 seconds setTimeout(() => { notifications.value = notifications.value.filter(n => n.id !== id) }, 3000) }) return (R) => (
{({ item }) => (R) => (
{item.message}
)}
) } ``` ## Server-Side Rendering with HTML Renderer ```javascript import { signal } from 'refui' import { createHTMLRenderer } from 'refui/html' const HTMLRenderer = createHTMLRenderer() const Page = ({ title, content }) => { const timestamp = new Date().toISOString() return (R) => ( {title}

{title}

{content}
) } // Render to HTML string const instance = HTMLRenderer.render(null, Page, { title: 'My Page', content: 'Hello from SSR!' }) const html = HTMLRenderer.serialize(instance) console.log(html) // Output: My Page... // Use in Node.js server import http from 'http' http.createServer((req, res) => { const html = HTMLRenderer.serialize( HTMLRenderer.render(null, Page, { title: 'Server Response', content: `Request to ${req.url}` }) ) res.writeHead(200, { 'Content-Type': 'text/html' }) res.end(html) }).listen(3000) ``` ## Portal Component for Teleporting Elements ```javascript import { createPortal } from 'refui/extras' const [Inlet, Outlet] = createPortal() const ModalContent = () => (R) => ( ) const App = () => { const showModal = signal(false) return (R) => (

Main Content

{showModal.value && } {/* Portal outlet - renders Inlet content here */}
) } ``` ## Cached List Rendering for Performance ```javascript import { createCache, signal } from 'refui/extras' const ItemComponent = ({ id, text }) => { console.log('ItemComponent created for:', id) return (R) => (
  • {text} (ID: {id})
  • ) } const OptimizedList = () => { const cache = createCache(ItemComponent) // Add items cache.add( { id: 1, text: 'First' }, { id: 2, text: 'Second' }, { id: 3, text: 'Third' } ) // Replace all items const updateList = () => { cache.replace([ { id: 1, text: 'Updated First' }, { id: 4, text: 'Fourth' }, { id: 5, text: 'Fifth' } ]) } // Delete item const removeSecond = () => { const idx = cache.getIndex(item => item.id === 2) if (idx >= 0) cache.del(idx) } return (R) => (

    Total items: {cache.size()}

    ) } ``` ## UnKeyed List Rendering ```javascript import { UnKeyed, signal } from 'refui/extras' // UnKeyed is like For but without key tracking // Useful for simple lists that don't need diffing optimization const SimpleList = () => { const items = signal(['Apple', 'Banana', 'Cherry', 'Date']) const shuffle = () => { const shuffled = [...items.value].sort(() => Math.random() - 0.5) items.value = shuffled } return (R) => (

    Fruit List

    ) } ``` ## Dynamic Text Parsing with Parse Component ```javascript import { Parse, signal } from 'refui/extras' // Custom parser for markdown-like syntax const simpleMarkdownParser = (text, renderer) => { const parts = text.split(/(\*\*.*?\*\*|\*.*?\*)/g) return parts.map(part => { if (part.startsWith('**') && part.endsWith('**')) { const content = part.slice(2, -2) return (R) => {content} } else if (part.startsWith('*') && part.endsWith('*')) { const content = part.slice(1, -1) return (R) => {content} } return (R) => {part} }) } const MarkdownViewer = () => { const text = signal('This is *italic* and this is **bold** text!') const updateText = () => { text.value = 'New text with **bold** and *italic* formatting' } return (R) => (

    ) } ``` ## Custom Macros for DOM Manipulation ```javascript import { createDOMRenderer } from 'refui/dom' import { defaults } from 'refui/browser' import { signal, bind } from 'refui' // Create renderer with custom macros const DOMRenderer = createDOMRenderer({ ...defaults, macros: { tooltip(node, value) { bind((text) => { if (text) { node.setAttribute('data-tooltip', text) node.classList.add('has-tooltip') } else { node.removeAttribute('data-tooltip') node.classList.remove('has-tooltip') } }, value) }, autofocus(node, value) { if (value) { setTimeout(() => node.focus(), 0) } } } }) // Or register macros after creation DOMRenderer.useMacro({ name: 'animate', handler(node, value) { bind((animationName) => { if (animationName) { node.style.animation = animationName } }, value) } }) const App = () => { const tooltip = signal('Click me for info') const animation = signal('fadeIn 0.5s ease') return (R) => (
    Animated Content
    ) } DOMRenderer.render(document.body, App) ``` ## Hot Module Replacement Setup ```javascript // vite.config.js import { defineConfig } from 'vite' import refurbish from 'refurbish/vite' export default defineConfig({ plugins: [refurbish()], esbuild: { jsxFactory: 'R.c', jsxFragment: 'R.f' } }) // Component file with HMR // Counter.jsx import { signal } from 'refui' export const Counter = ({ expose }) => { const count = signal(0) const increment = () => count.value++ // Optionally expose state for parent access expose?.({ count }) return (R) => (

    Count: {count}

    ) } // HMR automatically preserves state when you edit the component! ``` ## Native App Rendering with NativeScript ```javascript import { Application } from '@nativescript/core' import { document } from 'dominative' import { signal } from 'refui' import { createDOMRenderer } from 'refui/dom' const DOMRenderer = createDOMRenderer({ doc: document }) const App = () => { const count = signal(0) const increment = () => { count.value += 1 } return (R) => ( <> ) } DOMRenderer.render(document.body, App) const create = () => document Application.run({ create }) ``` ## Summary rEFui provides a complete reactive framework solution with its fine-grained signal system at the core. The primary use cases include building single-page applications with automatic state management, creating server-side rendered pages with the HTML renderer, developing cross-platform applications for web and native environments, and building embedded UI systems for constrained devices. The signal system can be used standalone for any reactive programming needs, while the component system provides a clean, functional approach to UI composition. Version 0.8.0 introduces a refined component API with the `expose` prop pattern, which provides better ergonomics for component imperative handles and eliminates the need for `capture()` in async components. The extras module includes additional utilities like `UnKeyed` for untracked list rendering, `Parse` for dynamic text parsing, `createCache` for optimized list performance, and `createPortal` for teleporting elements. Integration with existing projects is straightforward through the modular architecture - you can use signals alone for state management, the DOM renderer for browser-based UIs, the HTML renderer for SSR, or custom renderers for specialized platforms. The built-in HMR support via refurbish makes development faster with state preservation across code changes. The framework's small size, zero dependencies, and TypeScript-friendly API make it an excellent choice for projects requiring reactive capabilities without the complexity and bundle size overhead of larger frameworks like React or Vue. With support for JSX, HTM templates, and direct createElement calls, rEFui adapts to your preferred development workflow while maintaining consistent reactive behavior across all rendering targets. The component reference system via `$ref` and `expose` props provides a powerful way to access DOM elements and component APIs imperatively when needed, while the built-in components (`If`, `For`, `Dynamic`, `Async`, `Render`) cover common UI patterns without requiring third-party libraries.