Migrating from React
A practical guide for React developers moving to WildflowerJS. This page maps React concepts to their WildflowerJS equivalents, compares the two approaches side by side, and walks through each pattern you will need to convert.
Mental Model Shift
React and WildflowerJS solve the same problem — keeping the UI in sync with application state — but their approaches differ at every level. Understanding these differences is more important than memorizing syntax.
| Concern | React | WildflowerJS |
|---|---|---|
| DOM strategy | Virtual DOM + reconciliation. React builds a virtual tree, diffs it against the previous tree, and patches the real DOM with the minimum set of changes. | Direct DOM updates. When state changes, WildflowerJS updates only the specific DOM elements that are bound to the changed property. No diffing step, no virtual tree. |
| Markup | JSX, JavaScript-embedded markup. HTML lives inside JavaScript functions and is compiled by Babel or SWC before the browser sees it. | Standard HTML with data-* attributes. Markup stays as HTML in .html files. No compilation step, no transpilation. |
| State mutation | Immutable. You must call setState or a setter from useState, typically with spread operators to create new objects/arrays. |
Direct mutation. this.count++, this.items.push(item), this.user.name = 'Alice'. The reactive proxy detects the change and triggers updates. |
| Update granularity | Component re-rendering. When state changes, the entire component function re-runs, producing a new virtual tree to diff. | Element-level updates. Only the DOM elements bound to the changed property are updated. Other elements in the component are untouched. |
| Derived values | Hooks with dependency arrays. useMemo(() => ..., [dep1, dep2]). You must manually list dependencies; stale closures occur if you forget one. |
Declarative computed properties. computed: { fullName() { return this.first + ' ' + this.last; } }. Dependencies are auto-tracked, with no array to maintain. |
Concept Mapping
Quick reference for translating React patterns to WildflowerJS equivalents:
| Concept | React | WildflowerJS |
|---|---|---|
| Component | function MyComp() { return <div>...</div>; } |
<div data-component="my-comp">...</div> + wildflower.component('my-comp', { ... }) |
| State | const [count, setCount] = useState(0) |
state: { count: 0 } then this.count++ |
| Computed / Memo | useMemo(() => items.filter(...), [items]) |
computed: { filtered() { return this.items.filter(...); } } |
| Side Effects | useEffect(() => { ... return cleanup; }, [deps]) |
init() { ... } and destroy() { ... } |
| Events | <button onClick={handleClick}> |
<button data-action="handleClick"> |
| Conditional | {show && <div>...</div>} or ternary |
<div data-show="show"> or <div data-render="show"> |
| List | {items.map(item => <li key={item.id}>...</li>)} |
<ul data-list="items" data-key="id"><template>...</template></ul> |
| Two-way binding | <input value={val} onChange={e => setVal(e.target.value)} /> |
<input data-model="val" /> |
| Global state | createContext + useContext, or Redux / Zustand |
wildflower.store('name', { ... }) + subscribe: { storeName: [...] } |
| Lifecycle | useEffect(() => { ... }, []) for mount, return for unmount |
init() for mount, destroy() for unmount |
| Routing | React Router: <Route path="/about" element={<About />} /> |
wildflower.routes({ '/about': { component: 'about-page' } }) |
| Styling | CSS modules, styled-components, or inline style={{ }} |
Standard CSS, data-bind-class, data-bind-style |
Side-by-Side: Todo App
The best way to see the differences is a complete example. Here is the same Todo application built in both frameworks.
React (functional component with hooks)
function TodoApp() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const remaining = useMemo(() =>
todos.filter(t => !t.done).length, [todos]);
function addTodo() {
if (!newTodo.trim()) return;
setTodos([...todos, {
id: Date.now(),
text: newTodo,
done: false
}]);
setNewTodo('');
}
function toggleTodo(id) {
setTodos(todos.map(t =>
t.id === id
? { ...t, done: !t.done }
: t
));
}
function removeTodo(id) {
setTodos(todos.filter(t =>
t.id !== id));
}
return (
<div>
<h3>Todos ({remaining} remaining)</h3>
<input
value={newTodo}
onChange={e =>
setNewTodo(e.target.value)}
onKeyDown={e =>
e.key === 'Enter' && addTodo()}
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() =>
toggleTodo(todo.id)}
/>
<span style={{
textDecoration: todo.done
? 'line-through'
: 'none'
}}>
{todo.text}
</span>
<button onClick={() =>
removeTodo(todo.id)}>
×
</button>
</li>
))}
</ul>
</div>
);
}
WildflowerJS (HTML + JS)
<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>
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);
}
});
- No immutable spreads:
this.todos.push()replacessetTodos([...todos, item]) - No toggle function:
data-model="done"on the checkbox handles two-way binding automatically - No dependency array: the
remainingcomputed property auto-tracks its dependency onthis.todos - No event wiring:
data-action="removeTodo"replacesonClick={() => removeTodo(todo.id)}; the list index arrives indetails.index - No build step: the HTML is valid as-is; no JSX compilation needed
Pattern-by-Pattern Migration
Component Definition
React components are JavaScript functions that return JSX. WildflowerJS components are HTML elements paired with a JavaScript definition object.
React
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// Usage:
<Greeting name="Alice" />
WildflowerJS
<div data-component="greeting">
<h1>Hello, <span data-bind="name"></span>!</h1>
</div>
wildflower.component('greeting', {
props: { name: { type: String, default: '' } }
});
<!-- Usage: -->
<div data-component="greeting"
data-prop-name="Alice"></div>
State Management
React requires setter functions and immutable updates. WildflowerJS uses direct property mutation on a reactive proxy.
React
function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({
name: 'Alice',
score: 0
});
function increment() {
setCount(c => c + 1);
}
function updateScore() {
// Must spread to create new object
setUser({ ...user, score: user.score + 10 });
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<p>{user.name}: {user.score}</p>
<button onClick={updateScore}>
+10 points
</button>
</div>
);
}
WildflowerJS
<div data-component="counter">
<p>Count: <span data-bind="count"></span></p>
<button data-action="increment">+1</button>
<p><span data-bind="user.name"></span>:
<span data-bind="user.score"></span></p>
<button data-action="updateScore">
+10 points
</button>
</div>
wildflower.component('counter', {
state: {
count: 0,
user: { name: 'Alice', score: 0 }
},
increment() {
this.count++;
},
updateScore() {
// Direct mutation: no spread needed
this.user.score += 10;
}
});
Computed Values
React's useMemo requires an explicit dependency array. WildflowerJS computed properties auto-track dependencies.
React
function ProductList({ products }) {
const [filter, setFilter] = useState('');
// Must list dependencies manually
const filtered = useMemo(() =>
products.filter(p =>
p.name.toLowerCase()
.includes(filter.toLowerCase())
),
[products, filter] // forget one = stale data
);
const total = useMemo(() =>
filtered.reduce((sum, p) =>
sum + p.price, 0),
[filtered]
);
return (
<div>
<input value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Search..." />
<p>{filtered.length} items, ${total}</p>
</div>
);
}
WildflowerJS
<div data-component="product-list">
<input data-model="filter"
placeholder="Search..." />
<p><span data-bind="filtered.length"></span>
items, $<span data-bind="total"></span></p>
</div>
wildflower.component('product-list', {
state: {
products: [],
filter: ''
},
computed: {
// Dependencies auto-tracked
filtered() {
return this.products.filter(p =>
p.name.toLowerCase()
.includes(this.filter.toLowerCase())
);
},
total() {
return this.filtered.reduce(
(sum, p) => sum + p.price, 0
);
}
}
});
total depends on filtered, which depends on products and filter. WildflowerJS tracks the entire chain automatically. In React, you would need to list the correct dependency for each useMemo call.
Side Effects
React uses useEffect for mount/unmount logic and for responding to state changes. WildflowerJS splits these into init()/destroy() lifecycle methods and watch handlers.
React
function Timer() {
const [seconds, setSeconds] = useState(0);
// Mount + cleanup
useEffect(() => {
const id = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
return () => clearInterval(id);
}, []);
// Respond to state change
useEffect(() => {
document.title = `${seconds}s elapsed`;
}, [seconds]);
return <p>{seconds} seconds</p>;
}
WildflowerJS
<div data-component="timer">
<p><span data-bind="seconds"></span> seconds</p>
</div>
wildflower.component('timer', {
state: { seconds: 0 },
// watch must be a top-level definition key
// (not assigned inside init)
watch: {
seconds() {
document.title =
`${this.seconds}s elapsed`;
}
},
init() {
this._interval = setInterval(() => {
this.seconds++;
}, 1000);
},
destroy() {
clearInterval(this._interval);
}
});
Event Handling
React passes inline functions or references. WildflowerJS uses data-action to bind events to method names.
React
function Form() {
const [name, setName] = useState('');
function handleSubmit(e) {
e.preventDefault();
console.log('Submitted:', name);
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={e =>
setName(e.target.value)}
/>
<button type="submit">Go</button>
</form>
);
}
WildflowerJS
<div data-component="my-form">
<form data-action="submit.prevent:handleSubmit">
<input data-model="name" />
<button type="submit">Go</button>
</form>
</div>
wildflower.component('my-form', {
state: { name: '' },
handleSubmit() {
console.log('Submitted:', this.name);
}
});
.prevent, .stop, .enter, .escape directly in the data-action attribute, replacing the need for e.preventDefault() or e.stopPropagation() calls in your methods.
Conditional Rendering
React uses JavaScript expressions inside JSX. WildflowerJS uses data-show (toggles visibility) and data-render (adds/removes from DOM).
React
function Alert({ type, message }) {
return (
<div>
{type === 'error' && (
<div className="alert-error">
{message}
</div>
)}
{type === 'success' && (
<div className="alert-success">
{message}
</div>
)}
{!message && (
<p>No messages</p>
)}
</div>
);
}
WildflowerJS
<div data-component="alert-box">
<div class="alert-error"
data-show="type === 'error'">
<span data-bind="message"></span>
</div>
<div class="alert-success"
data-show="type === 'success'">
<span data-bind="message"></span>
</div>
<p data-show="!message">No messages</p>
</div>
wildflower.component('alert-box', {
state: {
type: '',
message: ''
}
});
data-show vs data-render: Use data-show when elements toggle frequently (it sets display: none). Use data-render when elements are rarely shown or contain heavy sub-components (it removes them from the DOM entirely, like React's conditional {cond && <Component />}).
List Rendering
React uses .map() inside JSX with a key prop. WildflowerJS uses data-list with a <template>.
React
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
<strong>{user.name}</strong>
<span>{user.email}</span>
<button onClick={() =>
deleteUser(user.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
WildflowerJS
<ul data-list="users" data-key="id">
<template>
<li>
<strong data-bind="name"></strong>
<span data-bind="email"></span>
<button data-action="deleteUser">
Delete
</button>
</li>
</template>
</ul>
data-list template, data-bind references properties of the current item, not the parent component. So data-bind="name" means item.name. The action method receives the item index in details.index.
data-pool, WildflowerJS's pull-reactive rendering mode. Same template syntax, zero proxy overhead, with explicit update control via markDirty(). No virtualization library needed.
Forms and Two-Way Binding
React requires controlled components with value + onChange. WildflowerJS uses data-model for two-way binding.
React
function SignupForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [agree, setAgree] = useState(false);
const [role, setRole] = useState('user');
return (
<form>
<input type="email"
value={email}
onChange={e =>
setEmail(e.target.value)} />
<input type="password"
value={password}
onChange={e =>
setPassword(e.target.value)} />
<input type="checkbox"
checked={agree}
onChange={e =>
setAgree(e.target.checked)} />
<select value={role}
onChange={e =>
setRole(e.target.value)}>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</form>
);
}
WildflowerJS
<div data-component="signup-form">
<form>
<input type="email"
data-model="email" />
<input type="password"
data-model="password" />
<input type="checkbox"
data-model="agree" />
<select data-model="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</form>
</div>
wildflower.component('signup-form', {
state: {
email: '',
password: '',
agree: false,
role: 'user'
}
});
useState and onChange handler for every form field. WildflowerJS's data-model handles read and write in a single attribute. For a form with 10 fields, that eliminates 20 lines of React boilerplate.
Global State
React uses Context/useContext or external libraries (Redux, Zustand). WildflowerJS has a built-in store system.
React (Context + useContext)
// Define context
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider
value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Consume in any child
function ThemedButton() {
const { theme, setTheme } = useContext(
ThemeContext
);
return (
<button
className={theme}
onClick={() =>
setTheme(theme === 'light'
? 'dark' : 'light')
}>
Toggle ({theme})
</button>
);
}
WildflowerJS (Store)
// Define store (once, anywhere)
wildflower.store('theme', {
state: { mode: 'light' },
toggle() {
this.mode = this.mode === 'light'
? 'dark' : 'light';
}
});
<!-- Consume in any component -->
<!-- Use $storeName.property to bind directly to store state -->
<div data-component="themed-button">
<button data-action="toggle"
data-bind-class="$theme.mode">
Toggle (<span data-bind="$theme.mode"></span>)
</button>
</div>
wildflower.component('themed-button', {
subscribe: ['theme'],
toggle() {
wildflower.getStore('theme').toggle();
}
});
<Provider>. WildflowerJS stores are globally accessible. Any component can subscribe to any store without changing the component hierarchy.
Common Gotchas for React Developers
These are the most frequent stumbling blocks when coming from React.
React habits die hard. You might write this.items = [...this.items, newItem] out of muscle memory. It works, but this.items.push(newItem) is simpler and triggers fine-grained updates: only the new list item is rendered, rather than replacing the entire list.
Computed properties auto-track every reactive property accessed during execution. There is no [deps] argument. If you find yourself wondering "what are the dependencies?", stop. The framework already knows.
In React, functions defined inside a component are new references on every render, which is why useCallback exists. WildflowerJS methods are defined once on the component definition and are stable references. There is no equivalent of useCallback because there is no problem to solve.
This is the biggest conceptual shift. When this.count changes, WildflowerJS does not re-run your component definition or re-evaluate your template. It updates only the DOM elements that have data-bind="count". Everything else is untouched.
React.memo prevents unnecessary re-renders of child components. In WildflowerJS, there are no re-renders to prevent. Each binding updates independently, so the problem React.memo solves does not exist.
data-action="doSomething" calls a method by string name. You cannot write data-action="() => doSomething(42)". If you need to pass context, use data-* attributes on the element and read them from the element parameter in your method: doSomething(event, element) { const id = element.dataset.id; }.
Migration Checklist
Follow these steps when converting a React application to WildflowerJS. Tackle one component at a time.
- Remove build tooling for JSX. Webpack/Vite/Babel configurations for JSX transpilation are no longer needed. WildflowerJS uses standard HTML and vanilla JavaScript. No compile step required. You can still use a bundler for other purposes if you choose.
-
Replace JSX with HTML +
data-*attributes. Convert each component's return statement from JSX to an HTML element withdata-component. Replace{variable}expressions withdata-bind="variable"on<span>elements. -
Convert
useStatetostateobject properties. Gather alluseStatecalls into a singlestate: { ... }object in the component definition. Replace setter calls (setCount(c => c + 1)) with direct mutation (this.count++). -
Convert
useMemoanduseCallbacktocomputedproperties. Move memoized values intocomputed: { ... }. Remove the dependency arrays entirely. Remove alluseCallbackwrappers. They have no equivalent because methods are already stable. -
Convert
useEffecttoinit()/destroy()lifecycle. Mount effects (useEffect(..., [])) go ininit(). Cleanup functions become thedestroy()method. State-watching effects becomethis.watch = { propName: callback }. -
Replace Context / Redux with
wildflower.store. Define stores withwildflower.store('name', { state: {...}, methodName() {...} }). Methods go at the top level, not inside amethodsblock. In components, usesubscribe: ['storeName']and bind with$storeName.property. Remove all<Provider>wrapper components. -
Replace React Router with WildflowerJS
RouteManager. Convert<Route>definitions towildflower.routes({ '/path': { component: 'name' } }). Replace<Link to="...">with<a href="..." data-route>. -
Remove performance optimizations that are no longer needed.
Delete
React.memo()wrappers,useCallback()calls, and manualshouldComponentUpdatelogic. These solve React-specific problems that do not exist in WildflowerJS's binding-level update model. - Test each component individually. After converting a component, verify it works in isolation before moving on. WildflowerJS components are self-contained, so you can migrate incrementally.