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> }}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 LiveClockmountruns once by definition — there’s no[]to remember, and no way to accidentally re-run setup every render. Teardown is thereturnfrom the same block, so setup and teardown sit together instead of in two places.- State only changes through an
action(tick).mountandeffectobserve; mutation is always a named action, so there’s one place to look for “what changesnow.” effect on(now)names its trigger. It’s not a positional array beside the body — it reads as “whennowchanges, do this,” and the compiler builds the dependency array from the name.
What disappeared
Section titled “What disappeared”- The empty
[]you must remember to add →mountruns once, by definition. [now]positional dependency arrays →effect on(now)— the trigger is named, and the dep array is generated from it.- Two concerns crammed into one
useEffectshape → amountblock and aneffect 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.
It’s not a new runtime
Section titled “It’s not a new runtime”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.