Migrating from Solid

Solid and WildflowerJS both use 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. They differ in syntax, JSX with signal getters against HTML with data-* attributes, and in state access. Solid requires explicit getter calls (count()) and setter functions (setCount()), while WildflowerJS resolves properties directly (this.count) and accepts plain assignment (this.count = 5).

What transfers: because both frameworks use fine-grained reactivity, your model of when things update carries over unchanged. The migration is mostly syntactic, replacing JSX and signal accessors with HTML attributes and direct property access.

Mental Model Shift

Solid and WildflowerJS are closer in architecture than most framework pairs. Both avoid the virtual DOM entirely and both use fine-grained reactivity. 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
What both do the same way: both frameworks update only the exact DOM nodes tied to changed data. If you set count to a new value, only the element displaying count is touched, not the entire component tree. That 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);
    }
});
What changed: The 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. Solid requires you to call the memo as a function, and 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
});
Tip: Most Solid 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 access: In Solid, you call index() as a function inside the callback. In WildflowerJS, action handlers inside lists receive details.index and details.item as their third argument, so there is no tracking function to call.

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. There is no compilation step, transpiler, or build tooling to set up. 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:

  1. Remove JSX build tooling. Babel, Vite Solid plugin, or other JSX transpilers are no longer needed. WildflowerJS requires no build step.
  2. Convert JSX to HTML with data-* attributes. Replace JSX expressions ({count()}) with data-bind="count". Replace event handlers (onClick) with data-action. Replace <Show>/<For> with data-show/data-list.
  3. Replace createSignal pairs with state properties. Gather all signals into a single state: {} object. Remove getter/setter destructuring.
  4. Replace createMemo with computed properties. Move memo functions into a computed: {} block. Remove the getter-call syntax (access as this.prop, not prop()).
  5. Replace createEffect with init() / destroy() lifecycle. One-time setup goes in init(). Cleanup goes in destroy(). Reactive DOM updates are handled by bindings automatically.
  6. Convert createStore/produce to wildflower.store() with direct mutation. Remove all produce() wrappers. Replace setState(path, value) with direct property assignment.
  7. Replace <Show>/<For>/<Switch> with data-show/data-list/data-render. Use <template> children for list items. Use multiple data-show or data-render elements for conditional branches.
  8. Remove Solid Router, add RouteManager if needed. WildflowerJS includes a built-in RouteManager for SPA routing. Replace <Router>/<Route> with data-route configuration.
  9. 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.
After the migration: there is no build tooling and no compilation step, the fine-grained reactivity performs the same as Solid's, state access is simpler, and the templates are standard HTML that works in any browser, any editor, and any hosting environment.