Migrating from Solid
Solid and WildflowerJS share a core philosophy: fine-grained reactivity without a virtual DOM. Both frameworks update only the specific DOM nodes that change, avoiding the overhead of diffing an entire tree. The key differences are syntax — JSX with signal getters vs. HTML with data-* attributes — and state access patterns: Solid requires explicit getter calls (count()) and setter functions (setCount()), while WildflowerJS resolves properties directly (this.count) and accepts plain assignment (this.count = 5).
Mental Model Shift
Solid and WildflowerJS are closer in architecture than most framework pairs. Both avoid the virtual DOM entirely. Fine-grained reactivity is the shared philosophy. Here is where the two approaches diverge:
| Aspect | Solid | WildflowerJS |
|---|---|---|
| Reactivity granularity | Fine-grained (no VDOM) | Fine-grained (no VDOM) |
| State access | Signal getter functions: count() |
Direct property access: this.count |
| State updates | setCount(5) / produce() |
this.count = 5 / direct mutation |
| Template syntax | JSX (components as functions) | HTML with data-* attributes |
| Derived state | createMemo() with explicit tracking |
computed: {} with auto-tracking |
| Side effects | createEffect() |
init() lifecycle hook |
| Build step | Required (JSX compilation) | None, works directly in the browser |
count to a new value, only the element displaying count is touched, not the entire component tree. This precision is preserved in the migration.
Concept Mapping
A quick reference for translating Solid concepts to their WildflowerJS equivalents:
| Solid | WildflowerJS | Notes |
|---|---|---|
createSignal(value) |
state: { prop: value } |
No getter/setter pair needed |
signal() (read) |
this.prop |
Direct property access, no function call |
setSignal(value) |
this.prop = value |
Plain assignment triggers reactivity |
createMemo(() => ...) |
computed: { prop() { ... } } |
Auto-tracked, cached until dependencies change |
createEffect(() => ...) |
init() lifecycle |
Runs once on mount; use watch for ongoing observation |
onMount(fn) |
init() |
Called after component is connected to the DOM |
onCleanup(fn) |
destroy() |
Called when component is removed from the DOM |
createStore([]) |
wildflower.store('name', {}) |
Named global stores with state, computed, and methods |
produce(fn) |
Direct mutation | this.items.push(item), no wrapper needed |
<Show when={...}> |
data-show / data-render |
data-show toggles display; data-render adds/removes from DOM |
<For each={...}> |
data-list |
With <template> child for item markup |
<Switch> / <Match> |
Multiple data-show elements |
Or use data-render for mutually exclusive sections |
createContext() |
wildflower.store('name', {}) |
Stores are globally accessible; no provider tree needed |
useContext(ctx) |
subscribe: { storeName: [...] } |
Declarative subscription with automatic cleanup |
Side-by-Side: Todo List
A complete todo application in both frameworks, showing how the same functionality maps across:
Solid
import { createSignal, createMemo, For } from 'solid-js';
import { createStore, produce } from 'solid-js/store';
function TodoApp() {
const [todos, setTodos] = createStore([]);
const [newTodo, setNewTodo] = createSignal('');
const remaining = createMemo(() =>
todos.filter(t => !t.done).length);
function addTodo() {
if (!newTodo().trim()) return;
setTodos(produce(t =>
t.push({ id: Date.now(), text: newTodo(), done: false })));
setNewTodo('');
}
function toggleTodo(index) {
setTodos(index, 'done', d => !d);
}
function removeTodo(index) {
setTodos(produce(t => t.splice(index, 1)));
}
return (
<div>
<h3>Todos ({remaining()} remaining)</h3>
<input value={newTodo()}
onInput={e => setNewTodo(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addTodo()} />
<button onClick={addTodo}>Add</button>
<ul>
<For each={todos}>
{(todo, index) => (
<li>
<input type="checkbox" checked={todo.done}
onChange={() => toggleTodo(index())} />
<span style={{ 'text-decoration':
todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => removeTodo(index())}>×</button>
</li>
)}
</For>
</ul>
</div>
);
}
WildflowerJS
HTML
<div data-component="todo-app">
<h3>Todos (<span data-bind="remaining">0</span> remaining)</h3>
<input data-model="newTodo"
data-action="keydown.enter:addTodo" />
<button data-action="addTodo">Add</button>
<ul data-list="todos" data-key="id">
<template>
<li>
<input type="checkbox" data-model="done" />
<span data-bind="text"
data-bind-style="{ textDecoration: done ? 'line-through' : 'none' }"></span>
<button data-action="removeTodo">×</button>
</li>
</template>
</ul>
</div>
JavaScript
wildflower.component('todo-app', {
state: { todos: [], newTodo: '' },
computed: {
remaining() {
return this.todos.filter(t => !t.done).length;
}
},
addTodo() {
if (!this.newTodo.trim()) return;
this.todos.push({
id: Date.now(),
text: this.newTodo,
done: false
});
this.newTodo = '';
},
removeTodo(event, element, details) {
this.todos.splice(details.index, 1);
}
});
toggleTodo function is gone entirely. The data-model="done" on the checkbox handles two-way binding automatically. The produce() wrapper around array mutations is replaced with direct .push() and .splice() calls. The signal getter remaining() becomes a computed property accessed as data-bind="remaining".
Pattern-by-Pattern Migration
Signals → State Properties
Solid signals use a getter/setter pair. WildflowerJS state properties are plain values on this, with no function calls for reading or writing.
Solid
const [count, setCount] = createSignal(0);
const [name, setName] = createSignal('');
const [items, setItems] = createStore([]);
// Read
console.log(count());
console.log(name());
// Write
setCount(count() + 1);
setName('Alice');
setItems(produce(s => s.push(item)));
WildflowerJS
wildflower.component('example', {
state: { count: 0, name: '', items: [] },
someMethod() {
// Read
console.log(this.count);
console.log(this.name);
// Write
this.count++;
this.name = 'Alice';
this.items.push(item);
}
});
Derived Values: createMemo → Computed
Both are cached and only re-evaluate when dependencies change. The difference: Solid requires you to call the memo as a function; WildflowerJS resolves computed properties like any other property.
Solid
const [price, setPrice] = createSignal(10);
const [quantity, setQuantity] = createSignal(2);
const total = createMemo(() =>
price() * quantity());
const formatted = createMemo(() =>
`$${total().toFixed(2)}`);
// Access: total(), formatted()
WildflowerJS
wildflower.component('invoice', {
state: { price: 10, quantity: 2 },
computed: {
total() {
return this.price * this.quantity;
},
formatted() {
return `$${this.total.toFixed(2)}`;
}
}
});
// Access: this.total, this.formatted
// Or in HTML: data-bind="total", data-bind="formatted"
Effects: createEffect → init Lifecycle
Solid's createEffect runs whenever its tracked signals change. WildflowerJS handles most of those cases automatically through bindings. For one-time setup logic, use the init() lifecycle hook.
Solid
const [count, setCount] = createSignal(0);
// Runs on mount + whenever count changes
createEffect(() => {
console.log('Count is now', count());
});
// One-time setup
onMount(() => {
fetchData();
});
// Cleanup
onCleanup(() => {
clearInterval(timer);
});
WildflowerJS
wildflower.component('example', {
state: { count: 0 },
// One-time setup on mount
init() {
this.fetchData();
},
// Cleanup on unmount
destroy() {
clearInterval(this.timer);
}
// Reactivity is handled by bindings:
// data-bind="count" updates automatically
// No explicit effect needed
});
createEffect calls exist to keep the DOM in sync with state. In WildflowerJS, data-bind, data-show, and data-list handle that automatically. You only need init() for truly imperative setup like starting timers, fetching data, or integrating third-party libraries.
Event Handling: onClick → data-action
Solid uses JSX event attributes with inline functions. WildflowerJS uses data-action to call named methods.
Solid
// Click (default)
<button onClick={handleClick}>Go</button>
// With arguments
<button onClick={() => remove(id)}>×</button>
// Other events
<input onInput={e => setName(e.target.value)} />
<form onSubmit={e => {
e.preventDefault();
save();
}}>
WildflowerJS
<!-- Click (default event) -->
<button data-action="handleClick">Go</button>
<!-- In a list, details.index is available -->
<button data-action="remove">×</button>
<!-- Other events: prefix with event name -->
<input data-action="input:handleInput" />
<!-- Submit with prevent default -->
<form data-action="submit.prevent:save">
Conditional Rendering: Show → data-show / data-render
Solid's <Show> component conditionally renders children. WildflowerJS offers two attributes: data-show (CSS display toggle, keeps element in DOM) and data-render (adds/removes element from DOM).
Solid
// Conditional render
<Show when={isLoggedIn()}>
<Dashboard />
</Show>
// With fallback
<Show when={isLoggedIn()}
fallback={<Login />}>
<Dashboard />
</Show>
// Multi-branch
<Switch>
<Match when={status() === 'loading'}>
<Spinner />
</Match>
<Match when={status() === 'error'}>
<Error />
</Match>
<Match when={status() === 'ready'}>
<Content />
</Match>
</Switch>
WildflowerJS
<!-- CSS toggle (stays in DOM) -->
<div data-show="isLoggedIn">
<div data-component="dashboard">...</div>
</div>
<!-- DOM insertion/removal -->
<div data-render="isLoggedIn">
<div data-component="dashboard">...</div>
</div>
<div data-render="!isLoggedIn">
<div data-component="login">...</div>
</div>
<!-- Multi-branch with data-show -->
<div data-show="status == 'loading'">
<div data-component="spinner">...</div>
</div>
<div data-show="status == 'error'">
<div data-component="error-view">...</div>
</div>
<div data-show="status == 'ready'">
<div data-component="content-view">...</div>
</div>
List Rendering: For → data-list
Solid's <For> iterates with a callback. WildflowerJS uses data-list with a <template> child that defines the item markup. Inside the template, data-bind resolves against each item.
Solid
<For each={todos()}>
{(todo, index) => (
<li>
<span>{todo.text}</span>
<button onClick={() =>
removeTodo(index())}>
×
</button>
</li>
)}
</For>
WildflowerJS
<ul data-list="todos" data-key="id">
<template>
<li>
<span data-bind="text"></span>
<button data-action="removeTodo">
×
</button>
</li>
</template>
</ul>
// In the component definition:
removeTodo(event, element, details) {
// details.index = position in array
// details.item = the todo object
this.todos.splice(details.index, 1);
}
index() as a function inside the callback. In WildflowerJS, action handlers inside lists receive details.index and details.item as their third argument. No tracking function needed.
Store / Context: createStore → wildflower.store
Solid's stores require produce() for deep mutations and createContext/useContext for sharing. WildflowerJS stores are globally named, support direct mutation, and components subscribe declaratively.
Solid
// Define store
const [state, setState] = createStore({
user: { name: '', role: '' },
items: []
});
// Update nested property
setState('user', 'name', 'Alice');
// Array mutation
setState(produce(s =>
s.items.push({ id: 1, text: 'New' })));
// Share via context
const AppContext = createContext();
<AppContext.Provider value={[state, setState]}>
{props.children}
</AppContext.Provider>
// Consume in child
const [state, setState] = useContext(AppContext);
WildflowerJS
// Define store (globally accessible by name)
wildflower.store('app', {
state: {
user: { name: '', role: '' },
items: []
},
updateUser(name) {
this.user.name = name; // Direct mutation
},
addItem(item) {
this.items.push(item); // Direct mutation
}
});
// Subscribe from any component
wildflower.component('profile', {
subscribe: { app: ['user'] },
init() {
// this.stores.app.user is reactive
console.log(this.stores.app.user.name);
}
});
Forms: value + onInput → data-model
Solid requires manual wiring of value and event handler for two-way binding. WildflowerJS uses a single data-model attribute.
Solid
const [name, setName] = createSignal('');
const [agreed, setAgreed] = createSignal(false);
<input type="text"
value={name()}
onInput={e => setName(e.target.value)} />
<input type="checkbox"
checked={agreed()}
onChange={e => setAgreed(e.target.checked)} />
<select value={choice()}
onChange={e => setChoice(e.target.value)}>
<option value="a">A</option>
<option value="b">B</option>
</select>
WildflowerJS
<input type="text" data-model="name" />
<input type="checkbox" data-model="agreed" />
<select data-model="choice">
<option value="a">A</option>
<option value="b">B</option>
</select>
wildflower.component('my-form', {
state: {
name: '',
agreed: false,
choice: 'a'
}
});
Common Gotchas
No getter calls
Write this.count, not count(). Properties resolve automatically. There are no signal accessor functions in WildflowerJS. In templates, data-bind="count" handles it.
No setter functions
Write this.count = 5, not setCount(5). Direct assignment triggers the reactive system. There are no set* functions to import or call.
No produce()
Mutate arrays and objects directly: this.items.push(item), this.items.splice(index, 1), this.user.name = 'Alice'. No wrapper functions needed; the proxy-based reactivity system detects mutations at the property level.
No createEffect for init logic
Use the init() lifecycle hook for setup that runs once on mount. For ongoing reactive updates, bindings (data-bind, data-show, etc.) handle DOM synchronization automatically without explicit effects.
No JSX
Templates are plain HTML with data-* attributes. No compilation step, no transpiler, no build tooling required. The HTML is valid as-is and can be served from any static file host.
Reactivity is similar, that is a good thing
Both Solid and WildflowerJS track at the property level, so your mental model of when updates happen transfers directly. If you understood why createMemo only re-runs when its tracked signals change, you already understand how computed works in WildflowerJS.
Migration Checklist
A step-by-step plan for converting a Solid application to WildflowerJS:
- Remove JSX build tooling. Babel, Vite Solid plugin, or other JSX transpilers are no longer needed. WildflowerJS requires no build step.
-
Convert JSX to HTML with
data-*attributes. Replace JSX expressions ({count()}) withdata-bind="count". Replace event handlers (onClick) withdata-action. Replace<Show>/<For>withdata-show/data-list. -
Replace
createSignalpairs with state properties. Gather all signals into a singlestate: {}object. Remove getter/setter destructuring. -
Replace
createMemowith computed properties. Move memo functions into acomputed: {}block. Remove the getter-call syntax (access asthis.prop, notprop()). -
Replace
createEffectwithinit()/destroy()lifecycle. One-time setup goes ininit(). Cleanup goes indestroy(). Reactive DOM updates are handled by bindings automatically. -
Convert
createStore/producetowildflower.store()with direct mutation. Remove allproduce()wrappers. ReplacesetState(path, value)with direct property assignment. -
Replace
<Show>/<For>/<Switch>withdata-show/data-list/data-render. Use<template>children for list items. Use multipledata-showordata-renderelements for conditional branches. -
Remove Solid Router, add RouteManager if needed. WildflowerJS includes a built-in
RouteManagerfor SPA routing. Replace<Router>/<Route>withdata-routeconfiguration. - Test each component. Verify that state updates, computed properties, list rendering, and conditional rendering all work as expected. The fine-grained reactivity model is the same, so behavior should match.