Migrating from React¶
Wybthon will feel familiar to React developers, but the underlying model is intentionally different. The key shift: components run once, not on every render.
This guide maps common React idioms to Wybthon equivalents and calls out the pitfalls people hit most often.
TL;DR¶
| React | Wybthon |
|---|---|
useState(0) |
count, set_count = create_signal(0) |
useEffect(fn, deps) |
create_effect(fn) |
useMemo(() => fn, deps) |
create_memo(fn) |
useContext(Ctx) |
use_context(Ctx) |
useRef() |
Ref() from wybthon |
<Suspense fallback={...} /> |
Suspense |
<ErrorBoundary /> |
ErrorBoundary |
lazy(() => import('./X')) |
lazy |
useReducer |
create_signal + plain functions |
Components run once¶
The single biggest change. In React, your component function runs on every render and useState/useEffect work because of hook rules. In Wybthon:
@component
def Counter():
count, set_count = create_signal(0)
print("Counter body running")
return button("count: ", count, on_click=lambda _e: set_count(count() + 1))
You'll see "Counter body running" exactly once, no matter how many clicks. The count accessor passed into button(...) becomes a reactive hole, so only that text node updates. Read Mental model for the formal definition.
Implications¶
- No dependency arrays. Effects subscribe to whatever signals they read while running.
- No
useCallback/useMemofor stability; closures don't get re-created. - No "stale closure" bugs from missing deps.
State and effects¶
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `count ${count}`;
}, [count]);
becomes
The effect re-runs because it reads count() while tracking. There's no dependency array.
Props¶
In React, props are a frozen object per render. In Wybthon, every prop is a callable accessor. Pass it through; don't unpack:
becomes
If you destructure a prop into a local variable, you freeze it at mount and lose reactivity. The dev-mode warning warn_destructured_prop will catch this.
Context¶
becomes
Wybthon's Provider is signal-backed: changing the value updates consumers without unmounting them.
Lists¶
React's array.map(item => <Row key={item.id} />) becomes:
For keys by reference identity by default. Use key= only if you need to rekey on a derived value. See Flow control.
Conditional rendering¶
Replace JSX ternaries with Show:
when accepts a getter so the condition is reactive without re-rendering the whole tree.
Refs and DOM access¶
const ref = useRef(null);
useEffect(() => { ref.current.focus(); }, []);
return <input ref={ref} />;
becomes
from wybthon import Ref, on_mount
ref = Ref()
on_mount(lambda: ref.current.focus())
return input_(ref=ref)
See DOM Interop.
Async data¶
React + Suspense is similar in spirit but Wybthon's create_resource is more direct:
data = create_resource(query, fetch_data)
return Suspense(fallback=lambda: p("Loading"),
children=lambda: span(lambda: data()["title"]))
See Suspense and Lazy Loading.
Things you can stop doing¶
useCallback/useMemofor identity stability. Closures aren't re-created.React.memo. There's nothing to memoize; components don't re-render.- Hook rules and exhaustive deps lints. State/effect creation is just a function call.
keyon every list item by index. Keyed identity is automatic viaFor.
Things to watch out for¶
- Don't read props eagerly.
name()inside the body freezes the value. Passnameitself or read inside an effect. - Don't recreate components inside the body. Define them at module scope. Components are cheap to mount but creating them inside a render does not re-render anything either way, so just define them once.
if/elsein the body short-circuits. UseShow/Switchfor conditional subtrees.
Cheat sheet¶
from wybthon import (
component, create_signal, create_effect, create_memo,
on_mount, on_cleanup, Show, For, Switch, Match,
create_context, Provider, use_context,
Suspense, ErrorBoundary, lazy,
)
Next steps¶
- Read Mental model.
- Walk through Authoring patterns for idiomatic recipes.
- Browse Examples for full apps.