# 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
Click me! Count is {count}
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) => (
{label}
{children.length > 0 && {children} }
)
}
const App = () => (R) => (
(Esc)
)
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) => (
)
}
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 }} />
Reset
Add 10
Focus Input
)
}
```
## 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 }
{isLoggedIn.value ? 'Logout' : 'Login'}
)
}
// 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) => (
)
}
```
## Dynamic Components
```javascript
import { signal, Dynamic } from 'refui'
const HomeView = () => (R) =>
const AboutView = () => (R) =>
const ContactView = () => (R) =>
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) => (
currentRoute.value = 'home'}>Home
currentRoute.value = 'about'}>About
currentRoute.value = 'contact'}>Contact
)
}
```
## 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) => (
selectedUserId.value++}>Next User
)
}
```
## 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 = !showChart.value}>
Toggle Chart
{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) => (
{items.map(item => {item.text} )}
)
})
const App = () => {
const data = signal([1, 2, 3, 4, 5])
const counter = signal(0)
return (R) => (
counter.value++}>
Re-render ({counter})
({ 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
{isRunning.value ? 'Pause' : 'Resume'}
)
}
```
## 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) => (
)
}
}
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}
Start Action
)
}
```
## 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) => Start }
{(R) => Loading...
}
{(R) => (
Success!
state.value = 'idle'}>Reset
)}
{(R) => (
Error occurred
state.value = 'idle'}>Retry
)}
)
}
```
## 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}
)}
triggerNotify('success')}>
Success
triggerNotify('error')}>
Error
triggerNotify('info')}>
Info
)
}
```
## 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) => (
Modal Title
This content is rendered in the portal outlet!
Close
)
const App = () => {
const showModal = signal(false)
return (R) => (
Main Content
showModal.value = true}>
Open Modal
{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) => (
Update List
Remove Second
cache.clear()}>Clear All
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
{({ item, index }) => (R) => (
{index.value + 1}. {item}
)}
Shuffle
)
}
```
## 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) => (
)
}
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) => (
)
}
// 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) => (
<>
You have tapped {count} time(s)
Tap me!
>
)
}
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.