Skip to content

Setup, teardown, and reactions as named blocks

A live clock: start a timer on mount, stop it on unmount, and update the tab title whenever the time changes.

Two different concerns — run-once setup with teardown and react-to-a-change — that React funnels through one overloaded hook (useEffect), where the trigger is a positional array you can get wrong and the empty [] is a line you can forget.


React — one hook, two jobs, two footguns

Section titled “React — one hook, two jobs, two footguns”
import { useState, useEffect } from "react"
function LiveClock() {
const [now, setNow] = useState(() => new Date())
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000)
return () => clearInterval(timer) // forget this return → the interval leaks past unmount
}, []) // forget [] → a new interval every render
useEffect(() => {
document.title = now.toLocaleTimeString()
}, [now]) // a separate concern, hand-wired to its own dep array
return <p>{now.toLocaleTimeString()}</p>
}

The trigger for the second effect is [now]: positional, unchecked, easy to drift from the body. And the empty [] on the first is load-bearing — forget it and you open a new interval every render.

Reactra — each concern is its own named block

Section titled “Reactra — each concern is its own named block”
export component LiveClock {
state now: Date = new Date()
action tick() { now = new Date() } // state changes route through an action
mount {
const timer = setInterval(tick, 1000)
return () => clearInterval(timer) // teardown, paired with its setup
}
effect on(now) { document.title = now.toLocaleTimeString() } // trigger is explicit
view { <p>{now.toLocaleTimeString()}</p> }
}
Compiled React 19 — this is the file in your repo
import { useCallback, useEffect, useState } from "react"
export const LiveClock = () => {
const [now, setNow] = useState((new Date()) as Date)
const tick = useCallback(() => { setNow(new Date()) }, [])
useEffect(() => { const timer = setInterval(tick, 1000)
return () => clearInterval(timer) // teardown, paired with its setup
}, [])
useEffect(() => { document.title = now.toLocaleTimeString() }, [now])
useEffect(() => { const h = globalThis.__REACTRA_TEST__; if (h) h.update("LiveClock", { now, tick }) })
return (<> <p>{now.toLocaleTimeString()}</p> </>)
}
export default LiveClock
  • mount runs once by definition — there’s no [] to remember, and no way to accidentally re-run setup every render. Teardown is the return from the same block, so setup and teardown sit together instead of in two places.
  • State only changes through an action (tick). mount and effect observe; mutation is always a named action, so there’s one place to look for “what changes now.”
  • effect on(now) names its trigger. It’s not a positional array beside the body — it reads as “when now changes, do this,” and the compiler builds the dependency array from the name.

  • The empty [] you must remember to addmount runs once, by definition.
  • [now] positional dependency arrayseffect on(now) — the trigger is named, and the dep array is generated from it.
  • Two concerns crammed into one useEffect shape → a mount block and an effect on(…) block, each with one job.

What didn’t disappear: the teardown return. Reactra keeps it — but pairs it with its setup inside mount, instead of leaving it as a return you might forget.


mount compiles to useEffect(() => { …setup…; return () => { …teardown… } }, []); effect on(now) compiles to useEffect(() => { … }, [now]). The exact React you’d write — with the dep array placed for you.

Name the lifecycle. The compiler wires the hook.