Version: 1.0.0 A comprehensive guide for React developers transitioning to Eleva
This guide helps React developers understand Eleva by mapping familiar React concepts to their Eleva equivalents.
| React | Eleva | Notes |
|---|---|---|
useState(initial) |
signal(initial) |
No array destructuring needed |
setState(newValue) |
signal.value = newValue |
Direct assignment |
useEffect(() => {}, [dep]) |
signal.watch(fn) |
Automatic dependency |
useRef(initial) |
signal(initial) |
Same API for refs |
useMemo(() => val, [deps]) |
Regular function | Computed on access |
useCallback(fn, [deps]) |
Regular function | No memoization needed |
<Component prop={val} /> |
:prop="val" |
Attribute syntax |
{condition && <El />} |
${cond ? '<El />' : ''} |
Template literal |
onClick={() => fn()} |
@click="fn" |
Event syntax |
| JSX | Template strings | No transpilation |
React:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(c => c - 1)}>-</button>
</div>
);
}
Eleva:
const Counter = {
setup({ signal }) {
const count = signal(0);
return {
count,
increment: () => count.value++,
decrement: () => count.value--
};
},
template: (ctx) => `
<div>
<p>Count: ${ctx.count.value}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
`
};
Key differences:
[value, setValue].value assignment instead of setter functionReact:
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetchUser(userId)
.then(data => setUser(data))
.finally(() => setLoading(false));
}, [userId]);
useEffect(() => {
document.title = user?.name || 'Loading...';
}, [user]);
if (loading) return <p>Loading...</p>;
return <h1>{user.name}</h1>;
}
Eleva:
const UserProfile = {
setup({ signal, props }) {
const user = signal(null);
const loading = signal(true);
// Fetch user data
const fetchData = async (id) => {
loading.value = true;
user.value = await fetchUser(id);
loading.value = false;
};
// Watch user for side effects (like updating document title)
user.watch((userData) => {
document.title = userData?.name || 'Loading...';
});
return {
user,
loading,
// Fetch initial data after mount
onMount: () => fetchData(props.userId)
};
},
template: (ctx) => `
${ctx.loading.value
? '<p>Loading...</p>'
: `<h1>${ctx.user.value.name}</h1>`
}
`
};
Key differences:
props.userId is a plain value (parent passed :userId="id.value")onMount lifecycle hook to fetch data after component mounts.watch() method for side effectsReact:
import { useRef, useEffect } from 'react';
function TextInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="text" />;
}
Eleva:
const TextInput = {
setup({ signal }) {
// For DOM references, query after render
const focusInput = () => {
queueMicrotask(() => {
document.querySelector('#my-input')?.focus();
});
};
// Call on mount
focusInput();
return {};
},
template: () => `
<input id="my-input" type="text" />
`
};
For mutable values:
// React
const renderCount = useRef(0);
renderCount.current++;
// Eleva - use signal for mutable values
const renderCount = signal(0);
renderCount.value++;
React:
import { useMemo, useCallback } from 'react';
function ExpensiveList({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item => item.name.includes(filter));
}, [items, filter]);
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
Eleva:
const ExpensiveList = {
setup({ props }) {
// Computed values - just use functions
// They're called during template execution
// Note: props can be Signals or values depending on what parent passes
// This example assumes parent passed values (e.g., :items="items.value")
const filteredItems = () => {
return props.items.filter(item =>
item.name.includes(props.filter)
);
};
// No useCallback needed - functions aren't recreated on render
const handleClick = (id) => {
console.log('Clicked:', id);
};
return { filteredItems, handleClick };
},
template: (ctx) => `
<ul>
${ctx.filteredItems().map(item => `
<li key="${item.id}" @click="() => handleClick(${item.id})">
${item.name}
</li>
`).join('')}
</ul>
`
};
Key differences:
useMemo needed; Eleva’s batching handles optimizationuseCallback needed; functions are stableReact:
// ThemeContext.js
const ThemeContext = createContext('light');
// App.js
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value=>
<Header />
<Main />
</ThemeContext.Provider>
);
}
// Header.js
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header className={theme}>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</header>
);
}
Eleva:
import Eleva from "eleva";
import { Store } from "eleva/plugins";
const app = new Eleva("App");
// Global store replaces Context
app.use(Store, {
state: {
theme: "light"
},
actions: {
toggleTheme: (state) => {
state.theme.value = state.theme.value === "light" ? "dark" : "light";
}
}
});
// Header component
app.component("Header", {
setup({ store }) {
return {
theme: store.state.theme,
toggleTheme: () => store.dispatch("toggleTheme")
};
},
template: (ctx) => `
<header class="${ctx.theme.value}">
<button @click="toggleTheme">Toggle Theme</button>
</header>
`
});
Key differences:
setup({ store })React:
// Parent
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text, done: false }]);
};
return (
<div>
<TodoForm onAdd={addTodo} />
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</div>
);
}
// Child
function TodoItem({ todo }) {
return <li>{todo.text}</li>;
}
Eleva:
// Parent
app.component("TodoList", {
setup({ signal }) {
const todos = signal([]);
const addTodo = (text) => {
todos.value = [...todos.value, { id: Date.now(), text, done: false }];
};
return { todos, addTodo };
},
template: (ctx) => `
<div>
<div class="todo-form" :on-add="addTodo"></div>
${ctx.todos.value.map(todo => `
<div key="${todo.id}" class="todo-item" :todo="todo"></div>
`).join('')}
</div>
`,
children: {
".todo-form": "TodoForm",
".todo-item": "TodoItem"
}
});
// Child
app.component("TodoItem", {
setup({ props }) {
return { todo: props.todo };
},
template: (ctx) => `
<li>${ctx.todo.value.text}</li>
`
});
Key differences:
children object, not JSX:prop attributesReact Router:
import { BrowserRouter, Routes, Route, Link, useParams } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/users">Users</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:id" element={<UserProfile />} />
</Routes>
</BrowserRouter>
);
}
function UserProfile() {
const { id } = useParams();
return <h1>User {id}</h1>;
}
Eleva Router:
import Eleva from "eleva";
import { Router } from "eleva/plugins";
const app = new Eleva("App");
const router = app.use(Router, {
mode: "history", // or "hash" for no server config
mount: "#app",
routes: [
{ path: "/", component: Home },
{ path: "/users", component: Users },
{ path: "/users/:id", component: UserProfile }
]
});
const UserProfile = {
setup({ router }) {
const userId = router.currentParams.value.id;
return { userId };
},
template: (ctx) => `
<h1>User ${ctx.userId}</h1>
`
};
// Navigation
router.navigate("/users/123");
// In templates
`<a href="#/users">Users</a>` // hash mode
Redux:
// store.js
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1 },
decrement: (state) => { state.value -= 1 },
incrementByAmount: (state, action) => { state.value += action.payload }
}
});
// Component
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
);
}
Eleva Store:
import Eleva from "eleva";
import { Store } from "eleva/plugins";
const app = new Eleva("App");
app.use(Store, {
state: {
count: 0
},
actions: {
increment: (state) => state.count.value++,
decrement: (state) => state.count.value--,
incrementByAmount: (state, amount) => state.count.value += amount
}
});
const Counter = {
setup({ store }) {
return {
count: store.state.count,
increment: () => store.dispatch("increment"),
incrementByAmount: (n) => store.dispatch("incrementByAmount", n)
};
},
template: (ctx) => `
<div>
<span>${ctx.count.value}</span>
<button @click="increment">+</button>
<button @click="() => incrementByAmount(5)">+5</button>
</div>
`
};
// React
{isLoading && <Spinner />}
{error ? <Error msg={error} /> : <Content data={data} />}
{items.length > 0 && <List items={items} />}
// Eleva
`${ctx.isLoading.value ? '<div class="spinner"></div>' : ''}`
`${ctx.error.value
? `<div class="error">${ctx.error.value}</div>`
: `<div class="content">${ctx.data.value}</div>`
}`
`${ctx.items.value.length > 0
? ctx.items.value.map(i => `<li key="${i.id}">${i.name}</li>`).join('')
: ''
}`
// React
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log({ name, email });
};
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={e => setName(e.target.value)} />
<input value={email} onChange={e => setEmail(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}
// Eleva
const Form = {
setup({ signal }) {
const name = signal('');
const email = signal('');
const handleSubmit = (e) => {
e.preventDefault();
console.log({ name: name.value, email: email.value });
};
return { name, email, handleSubmit };
},
template: (ctx) => `
<form @submit="handleSubmit">
<input
value="${ctx.name.value}"
@input="(e) => name.value = e.target.value"
/>
<input
value="${ctx.email.value}"
@input="(e) => email.value = e.target.value"
/>
<button type="submit">Submit</button>
</form>
`
};
// React
useEffect(() => {
console.log('Mounted');
return () => console.log('Unmounted');
}, []);
useEffect(() => {
console.log('Count changed:', count);
}, [count]);
// Eleva
setup({ signal }) {
const count = signal(0);
// Mount equivalent - runs once during setup
console.log('Mounted');
// Watch for changes
count.watch((newVal) => {
console.log('Count changed:', newVal);
});
// Unmount - if using router or manual unmount
return {
count,
onUnmount: () => console.log('Unmounted')
};
}
signal.value = newValueuseState with signal()useEffect with signal.watch()useMemo/useCallback (usually not needed)onClick with @clickqueueMicrotask)| ← Back to Migration Overview | From Vue → |