Coming from React
A practical translation guide: your React mental model mapped to Domphy equivalents.
The core shift
In React, the unit of composition is a component — a function that returns JSX. Props flow down, state lives inside the component, effects handle side-effects.
In Domphy, the unit is a plain object with patches applied via $. There are no components. State lives in toState values; patches apply behavior and style directly to native elements. The rendering model is simpler: a static description of the DOM + reactive values that update specific parts.
State
// React
const [count, setCount] = useState(0)
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Add</button>
// Domphy
import { toState } from "@domphy/core"
const count = toState(0)
const App = {
div: [
{ p: (l) => `Count: ${count.get(l)}` },
{ button: "Add", onClick: () => count.set(count.get() + 1) },
],
}
Key differences:
toState(initial)creates a mutable state value — no hook, no component scope- Read reactively:
(l) => count.get(l)— thel(listener) tracks which states a reactive function depends on - Write anywhere:
count.set(newValue)— no setter function from a hook tuple
Computed values
// React
const double = useMemo(() => count * 2, [count])
// Domphy
import { computed } from "@domphy/core"
const double = computed(() => count.get() * 2)
// read: (l) => double.get(l)
computed is lazy and cached — only recomputes when dependencies change.
Effects
// React
useEffect(() => {
document.title = `Count: ${count}`
return () => { /* cleanup */ }
}, [count])
// Domphy
import { effect } from "@domphy/core"
const stop = effect(() => {
document.title = `Count: ${count.get()}`
// return cleanup function (optional)
})
// call stop() to dispose
Effects auto-track dependencies like computed — no dependency array.
Object state / records
// React
const [user, setUser] = useState({ name: "Alice", age: 30 })
setUser(prev => ({ ...prev, age: 31 }))
// Domphy
import { RecordState } from "@domphy/core"
const user = new RecordState({ name: "Alice", age: 30 })
user.set("age", 31)
// per-key reactivity: listeners on "age" don't re-run on "name" change
RecordState gives you per-key reactivity — a listener watching user.get("age", l) only re-runs when age changes, not the whole object.
"Components" → patches + plain objects
React components are the unit of reuse. In Domphy, reuse happens two ways:
Function that returns an element (like a component):
// React
function Badge({ label, color }) {
return <span className={`badge badge-${color}`}>{label}</span>
}
// Domphy
function Badge(label: string, color: string) {
return { span: label, $: [badge({ color })] }
}
Patch function (for reusable behavior/style):
// React
function PrimaryButton({ label, onClick }) {
return <button className="btn-primary" onClick={onClick}>{label}</button>
}
// Domphy — patch instead of wrapper component
function primaryButton(): PartialElement {
return {
style: {
backgroundColor: (l) => themeColor(l, "shift-6", "primary"),
color: (l) => themeColor(l, "shift-15", "primary"),
// ...
}
}
}
// Use: { button: label, onClick, $: [primaryButton()] }
The difference: a patch doesn't render anything — it extends the element you already wrote.
Props
React components receive props as a function argument. Domphy elements are plain objects, so "props" are just object keys alongside the tag:
// React
<Button label="Save" color="primary" disabled={isLoading} />
// Domphy
{ button: "Save", $: [button({ color: "primary" })], disabled: isLoading }
Attributes like disabled, type, href, value are just keys on the element object. Patch props (color, size, variant) go inside the patch function call button({ ... }).
Event handlers
// React
<input value={name} onChange={(e) => setName(e.target.value)} />
// Domphy
{ input: null, type: "text", value: (l) => name.get(l), onInput: (e) => name.set((e.target as HTMLInputElement).value) }
- Event names:
onClick,onInput,onChange,onFocus,onBlur, etc. — same as React's synthetic events but native (no SyntheticEvent wrapper) - Controlled input:
value: (l) => state.get(l)+onInputwrite is loop-safe (setting.valueprogrammatically doesn't retriggeronInput)
Context
// React
const ThemeContext = createContext("light")
// provide: <ThemeContext.Provider value="dark">
// consume: const theme = useContext(ThemeContext)
// Domphy — context lives on the element tree via _context
const container = {
div: [...children],
_context: {
config: { value: toState("dark") }
}
}
// In a child patch's _onInsert:
const context = node.getContext("config")
const value = context.value.get(listener)
Context is defined as _context on a container element and accessed via node.getContext("name") in a patch's _onInsert lifecycle hook.
Lifecycle
// React
useEffect(() => {
// mount
const sub = api.subscribe(handler)
return () => sub.unsubscribe() // unmount
}, [])
// Domphy — lifecycle hooks in a patch or directly on the element
const patch: PartialElement = {
_onMount: (node) => {
const sub = api.subscribe(handler)
node._onBeforeRemove = (n, done) => {
sub.unsubscribe()
done()
}
},
}
Lifecycle hooks available:
_onSchedule(node)— before DOM insert (synchronous)_onInsert(node)— after DOM insert (DOM access available, before layout)_onMount(node)— after first paint_onBeforeRemove(node, done)— before removal; must calldone()to proceed_onRemove(node)— after removal
Refs
// React
const inputRef = useRef<HTMLInputElement>(null)
inputRef.current?.focus()
// Domphy — access DOM via _onMount
{
input: null,
type: "text",
_onMount: (node) => {
node.domElement?.focus()
},
}
node.domElement is the actual DOM element. Access it in _onMount (or later) when the element is in the DOM.
Lists and keys
// React
items.map(item => <li key={item.id}>{item.name}</li>)
// Domphy
(l) => items.get(l).map(item => ({ li: item.name, _key: item.id }))
_key is the Domphy equivalent of React's key prop. Required on dynamic lists (returned from a reactive function) so the reconciler can track reorders. The doctor's missing-key rule catches lists without it.
Code splitting
// React (Next.js)
const HeavyPage = lazy(() => import('./HeavyPage'))
// Domphy (@domphy/app)
const route = {
path: "/heavy",
lazy: () => import("./HeavyPage"),
}
@domphy/app supports lazy routes with automatic prefetching and SSR streaming.
Data fetching
// React Query
const { data, isLoading } = useQuery({ queryKey: ["user"], queryFn: fetchUser })
// @domphy/query — same API, Domphy adapter
import { createQuery } from "@domphy/query/domphy"
const query = createQuery(() => ({
queryKey: ["user"],
queryFn: fetchUser,
}))
// read: (l) => query.get(l).data
The adapter createQuery wraps TanStack query-core with Domphy's listener-based reactivity. The queryKey, queryFn, staleTime, and all other options are identical.
Routing
// React Router
<Route path="/users/:id" element={<UserPage />} />
// @domphy/router — TanStack Router port
import { createRoute } from "@domphy/router"
const route = createRoute({
path: "/users/$id",
component: () => ({ div: "User page" }),
})
The API is the TanStack Router API. If you know TanStack Router, you know @domphy/router.
Forms
// React Hook Form
const { register, handleSubmit } = useForm()
<input {...register("email")} />
// @domphy/form — TanStack Form port
import { createForm } from "@domphy/form/domphy"
const form = createForm(() => ({
defaultValues: { email: "" },
onSubmit: ({ value }) => console.log(value),
}))
const emailField = form.field("email")
const App = {
form: null,
onSubmit: (e) => { e.preventDefault(); form.handleSubmit() },
$: [/* form layout */],
}
const input = {
input: null,
type: "email",
value: (l) => emailField.value(l),
onInput: (e) => emailField.handleChange((e.target as HTMLInputElement).value),
}
Animations
// Framer Motion
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />
// Domphy — motion() patch (Web Animations API, no third-party dep)
{ div: "Content", $: [motion({ initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } })] }
motion() uses the Web Animations API natively. Enter/exit animations tie into Domphy's _onMount/_onBeforeRemove lifecycle — no extra library.
The mental model shift in one sentence
React: components own state and render JSX.
Domphy: plain objects describe DOM; patches inject behavior; state values update reactive parts.
Next steps
- Core syntax — the full element object format
- Reactivity — toState, computed, effect, batch
- Patches overview — the 85 built-in patches
- Quickstart — hands-on 5-minute intro