BarefootJS has three key ideas: a two-phase compiler, signal-based reactivity, and marker-driven hydration. This page explains how they fit together.
#Two-Phase Compilation
The compiler transforms a single JSX source file into two separate outputs:
JSX Source
↓
[Phase 1] Analyze + Transform → IR (Intermediate Representation)
↓
[Phase 2a] IR → Marked Template (server)
[Phase 2b] IR → Client JS (browser)Phase 1 parses the JSX once and produces a JSON IR tree. The IR captures the component structure, reactive expressions, event handlers, and type information — independent of any backend.
Phase 2 takes the IR and generates two outputs:
- Marked Template — An HTML template for your server, with
bf-*attributes marking interactive elements. The adapter determines the output format. - Client JS — A minimal script that creates signals, wires up effects, and binds event handlers to the marked elements.
#Example
Given this source:
"use client"
import { createSignal } from '@barefootjs/dom'
export function Counter({ initial = 0 }) {
const [count, setCount] = createSignal(initial)
return (
<div>
<p>{count()}</p>
<button onClick={() => setCount(n => n + 1)}>+1</button>
</div>
)
}Phase 1 produces an IR that records:
- A signal
countwith settersetCountand initial valueinitial - A reactive expression
count()atslot_0 - A click handler on the button at
slot_1
Phase 2a produces a server template:
export function Counter(props) {
return (
<div bf-s="Counter">
<p bf="slot_0">{props.initial ?? 0}</p>
<button bf="slot_1">+1</button>
</div>
)
}
{{define "Counter"}}
<div bf-s="{{.ScopeID}}">
<p bf="slot_0">{{.Initial}}</p>
<button bf="slot_1">+1</button>
</div>
{{end}}
Phase 2b produces client JS:
import { createSignal, createEffect, find, bind } from '@barefootjs/dom'
export function init(props) {
const [count, setCount] = createSignal(props.initial ?? 0)
const _slot_0 = find('[bf="slot_0"]')
createEffect(() => { _slot_0.textContent = String(count()) })
bind('[bf="slot_1"]', 'click', () => setCount(n => n + 1))
}The server renders the HTML. The browser runs only the client JS to make it interactive.
#Adapters
The IR is backend-agnostic. An adapter converts it to the template format your server needs:
| Adapter | Output | Backend |
|---|---|---|
HonoAdapter |
.hono.tsx |
Hono / JSX-based servers |
GoTemplateAdapter |
.tmpl + _types.go |
Go html/template |
The same JSX source produces correct output for each adapter. See Adapters for details.
#Signal-Based Reactivity
BarefootJS uses fine-grained reactivity inspired by SolidJS. The core primitives are signals, effects, and memos.
#Signals
A signal holds a reactive value. It returns a getter/setter pair:
const [count, setCount] = createSignal(0)
count() // Read: returns 0
setCount(5) // Write: set to 5
setCount(n => n + 1) // Write: updater functionThe getter is a function call — count(), not count. This is how the reactivity system tracks which effects depend on which signals.
#Effects
An effect runs a function whenever its signal dependencies change:
createEffect(() => {
console.log('Count is:', count())
})The first time it runs, the system records that count was read. When count changes, the effect re-runs automatically. No dependency array is needed.
#Memos
A memo is a cached derived value:
const doubled = createMemo(() => count() * 2)
doubled() // Returns the cached resultLike effects, memos track dependencies automatically. Unlike effects, they return a value and only recompute when dependencies change.
#How It Works
When a signal getter is called inside an effect, the effect subscribes to that signal. When the setter is called, all subscribed effects re-run. This happens at the expression level — only the specific DOM nodes that depend on a signal are updated.
setCount(1)
↓
Signal notifies subscribers
↓
Effect re-runs: _slot_0.textContent = String(count())
↓
Only <p> updates. The rest of the DOM is untouched.#Hydration Model
Hydration is the process of making server-rendered HTML interactive. BarefootJS uses a marker-driven approach.
#Hydration Markers
The compiler inserts bf-* attributes into the server template. These tell the client JS where to attach behavior:
| Marker | Purpose | Example |
|---|---|---|
bf-s |
Component scope boundary (~ prefix = child) |
<div bf-s="Counter_a1b2">, <div bf-s="~Item_c3d4"> |
bf |
Interactive element (slot) | <p bf="s0"> |
bf-h |
Hydration guard (runtime-only, prevents double hydration) | <div bf-s="Counter_a1b2" bf-h> |
bf-p |
Serialized props JSON | <div bf-p='{"initial":5}'> |
bf-c |
Conditional block | <div bf-c="s2"> |
bf-po |
Portal owner scope ID | <div bf-po="Dialog_a1b2"> |
bf-pi |
Portal container ID | <div bf-pi="bf-portal-1"> |
bf-pp |
Portal placeholder | <template bf-pp="bf-portal-1"> |
bf-i |
List item marker | <li bf-i> |
#Hydration Flow
- The server renders HTML with markers and embeds component props in
bf-pattributes - The browser loads the client JS
hydrate()finds allbf-selements withoutbf-h(uninitialized)- For each scope, the init function runs — creating signals, binding effects, attaching event handlers
- The runtime sets
bf-hon the scope element to prevent double initialization - The page is now interactive
Server HTML (static)
↓
Client JS loads
↓
hydrate("Counter", init)
↓
Find <div bf-s="Counter_a1b2"> without bf-h
↓
Read props from bf-p attribute
↓
Run init(): createSignal, createEffect, bind events
↓
Set bf-h on scope element (mark as initialized)
↓
Page is interactive#Scoped Queries
Each component only hydrates its own elements. The runtime's find() function searches within a scope boundary, excluding nested component scopes. This prevents components from interfering with each other.
<div bf-s="TodoApp_x1"> <!-- TodoApp scope -->
<h1 bf="slot_0">Todo</h1> <!-- belongs to TodoApp -->
<div bf-s="~TodoItem_y1"> <!-- TodoItem scope (~ = child, excluded from TodoApp queries) -->
<span bf="slot_0">Buy milk</span>
</div>
</div>When TodoApp's init calls find(scope, '[bf="slot_0"]'), it finds the <h1>, not the <span> inside TodoItem.
#The `"use client"` Directive
Components that use reactive primitives (createSignal, createEffect, etc.) must include the "use client" directive at the top of the file:
"use client"
import { createSignal } from '@barefootjs/dom'
export function Counter() {
const [count, setCount] = createSignal(0)
// ...
}This tells the compiler:
- Generate client JS for this component
- Add hydration markers to the server template
- Validate that reactive APIs are only used in client components
Without the directive, the compiler produces a server-only template with no client JS. A component without "use client" that tries to use createSignal will get an error:
error[BF001]: 'use client' directive required for components with createSignal
--> src/components/Counter.tsx:3:1
|
3 | import { createSignal } from '@barefootjs/dom'
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: Add "use client" at the top of the file#Security Boundary
"use client" marks a security boundary. Code in a client component is compiled into JavaScript that runs in the browser — meaning it is visible to the user. Never include secrets, database access, or other sensitive logic in a "use client" file.
// server-only.tsx — NO "use client"
// This code stays on the server. Safe for secrets.
export function UserList() {
const users = db.query('SELECT * FROM users')
return (
<ul>
{users.map(u => <li>{u.name}</li>)}
</ul>
)
}// counter.tsx — "use client"
// This code ships to the browser. No secrets here.
"use client"
import { createSignal } from '@barefootjs/dom'
export function Counter() {
const [count, setCount] = createSignal(0)
return <button onClick={() => setCount(n => n + 1)}>{count()}</button>
}#Server and Client Component Composition
Server components and client components have a clear composition rule:
- Server component → Client component: A server component can render a client component as a child. The server renders the HTML with hydration markers, and the client JS takes over in the browser.
- Client component → Client component: A client component can render other client components.
- Client component → Server component: Not allowed. A client component cannot import and render a server-only component, because server-only code does not exist on the client.
// Page.tsx — server component
// ✅ Can use client components as children
import { Counter } from './Counter' // "use client"
import { UserList } from './UserList' // server-only
export function Page() {
return (
<div>
<UserList /> {/* ✅ Server → Server */}
<Counter /> {/* ✅ Server → Client */}
</div>
)
}// Dashboard.tsx — "use client"
import { Counter } from './Counter' // ✅ Client → Client
import { UserList } from './UserList' // ❌ Client → Server (error)Think of "use client" as a one-way gate: once you cross into client territory, everything below must also be a client component.