Migrating from Solid¶
Wybthon is essentially SolidJS for Python. Most of the primitives have direct equivalents and the mental model is identical: components run once, signals drive fine-grained updates, and the ownership tree manages cleanup.
The differences are mostly cosmetic: Python instead of JavaScript, builder functions instead of JSX, and a few naming conventions to keep things idiomatic.
API mapping¶
| SolidJS | Wybthon |
|---|---|
createSignal(initial) |
create_signal(initial) |
createEffect(fn) |
create_effect(fn) |
createRenderEffect(fn) |
create_render_effect(fn) |
createComputed(fn) |
create_computed(fn) |
createMemo(fn, v, { equals }) |
create_memo(fn, equals=...) |
createReaction(onInvalidate) |
create_reaction(on_invalidate) |
onError(handler) |
on_error(handler) |
createDeferred(source) |
create_deferred(source) |
createUniqueId() |
create_unique_id() |
catchError(fn, handler) |
catch_error(fn, handler) |
createSelector(source) |
create_selector(source) |
mapArray / indexArray |
map_array / index_array |
mergeProps / splitProps |
merge_props / split_props |
children(fn) |
children(fn) |
getOwner / runWithOwner |
get_owner / run_with_owner |
createRoot(fn) |
create_root(fn) |
createResource(source, fetcher) |
create_resource(source, fetcher) |
createContext(default) / useContext |
create_context / use_context |
Ctx.Provider |
ctx.Provider(value=..., children=[...]) |
<Show when={...} fallback={...}> |
Show(when=..., fallback=...) |
<For each={...}> |
For(each=..., children=...) |
<Index each={...}> |
Index(each=..., children=...) |
<Switch> / <Match> |
Switch / Match |
<Dynamic component={...} /> |
Dynamic(component=...) |
<Portal mount={...}> |
Portal(mount=...) |
<ErrorBoundary fallback={...}> |
ErrorBoundary(fallback=...) |
<Suspense fallback={...}> |
Suspense(fallback=...) |
<SuspenseList revealOrder={...}> |
SuspenseList(reveal_order=...) |
lazy(() => import(...)) |
lazy(loader) |
onMount(fn) |
on_mount(fn) |
onCleanup(fn) |
on_cleanup(fn) |
batch(fn) |
batch(fn) |
untrack(fn) |
untrack(fn) |
on(deps, fn) |
on(deps, fn) |
createStore(initial) |
create_store(initial) |
createMutable(initial) |
create_mutable(initial) |
modifyMutable(state, modifier) |
modify_mutable(state, modifier) |
produce(fn) |
produce(fn) |
reconcile(data) |
reconcile(data, key="id") |
unwrap(store) |
unwrap(store) |
Templates¶
Solid uses JSX. Wybthon uses Python builders from wybthon.html:
from wybthon import component
from wybthon.html import p
@component
def Greeting(name):
return p("Hello, ", name, "!")
Tag helpers are defined for every standard HTML element. For custom elements, use h directly.
Props¶
Solid props are reactive getters on a proxy object. Wybthon props arrive as callables:
You can pass title straight through (creating a reactive hole) or read title() inside an effect. Destructuring (assigning the value to a local) freezes it at mount, just like Solid.
For ergonomic prop manipulation Wybthon offers merge_props and split_props, matching Solid's helpers of the same name:
from wybthon import component, merge_props, split_props
from wybthon.html import button
@component
def Button(props):
final = merge_props({"variant": "solid"}, props)
local, rest = split_props(final, ["label", "variant"])
return button(local["label"], class_=lambda: f"btn-{local['variant']}")
Signals and effects¶
Identical in spirit and behavior:
create_effect re-runs whenever signals it tracked during the previous run change. There's no manual dep array.
Execution semantics carry over¶
The behaviors you rely on in Solid hold in Wybthon too:
- Synchronous updates. Outside a
batch, asetpropagates before it returns. Afterset_count(1), bothcount()and any derived memo read the new value immediately. (Noawait, no microtask, no testsleep.) - Glitch-free. An effect reading several memos derived from one signal always sees a consistent combination and runs once per change, never on an intermediate state.
- Lazy memos.
create_memorecomputes only when read after a dependency changed, and skips notifying consumers when its value is unchanged (sameequals-based short-circuit as Solid). batchcoalesces writes and flushes once at the outermost boundary.
For and Index match Solid exactly: the mapping callback runs once
per unique item (or per index slot), and its result is cached. On list
changes only added items map, removed items dispose, and reorders move
existing DOM nodes. That means eager reads like str(item()) inside the
callback freeze at creation, just like destructuring props: pass the
accessor itself (or dynamic(lambda: ...)) where the value should stay
live.
Stores¶
from wybthon import create_store, produce, reconcile, unwrap
state, set_state = create_store({"count": 0, "items": []})
# Path-based writes:
set_state("count", lambda c: c + 1)
# Atomic multi-mutation update (Immer-style draft):
def update(s):
s.count += 1
s.items.append("new")
set_state(produce(update))
# Diff fresh server data in, preserving identity for unchanged items:
set_state("items", reconcile(fetched_items))
# Get the raw data back out:
raw = unwrap(state.items)
Stores wrap nested data in lazy proxies so reads are tracked at the leaf level, exactly like Solid.
Routing¶
from wybthon import Route, Router, Link
routes = [
Route(path="/", component=Home),
Route(path="/users/:id", component=User),
]
@component
def App():
return Router(routes=routes)
Wybthon's router supports nested routes, dynamic params, query parsing, and lazy components; see Routing.
What's intentionally different¶
- Naming. snake_case across the API (
create_signal, notcreateSignal). Component names stay PascalCase. - Signal equality. The default
equalspolicy is Python value equality (==with an identity fast path), not JS===. Passequals=lambda a, b: a is bwhen you want identity-only semantics, orequals=Falseto always notify. - Untracked reads. Signal and memo getters expose
.peek()(count.peek()), a shorthand foruntrack(count). - Imports. Pull from
wybthon(and optionallywybthon.htmlfor tag helpers). Dynamic. Usedynamic(lambda: ...)to inline a reactive computation; component-styleDynamicexists too.- JS interop. Use
pyodide.ffito talk to the host. See Pyodide guide.
What carries over directly¶
- The mental model (components run once, fine-grained reactivity).
- Ownership semantics:
on_cleanupattaches to the current owner. - Transitions and resources:
create_resourceintegrates withSuspense. - Patterns like keyed lists, conditional flows, and nested boundaries.
Next steps¶
- Read Mental model for the framework's core ideas.
- Explore Authoring patterns; many should look familiar.
- Browse the API reference for the full set of primitives.