Chapter 8: Conditional Rendering
Sometimes you need to show or hide parts of your UI based on state. Let’s add a “Clear Completed” button that only appears when there are completed todos.
The when Helper
The when helper conditionally renders elements:
import { when } from "@effex/dom";
when(condition, {
onTrue: () => $.div({}, $.of("Shown when true")),
onFalse: () => $.div({}, $.of("Shown when false")), // optional
})
condition- AReadable<boolean>or booleanonTrue- Rendered when condition is trueonFalse- Rendered when condition is false (optional)
When the condition changes, elements are added/removed from the DOM automatically.
Clear Completed Button
Let’s add a button that:
- Only shows when there are completed todos
- Removes all completed todos when clicked
First, create a derived value for whether there are any completed todos:
const hasCompletedTodos = Readable.map(todos, t => t.some(todo => todo.completed));
Then add the clear function:
const clearCompleted = () =>
todos.update(items => items.filter(t => !t.completed));
Now use when in the footer:
import { $, collect, each, Readable, Signal, when } from "@effex/dom";
// In the footer:
$.footer({ class: "footer" },
collect(
$.span(
{ class: "todo-count" },
$.of(Readable.map(todos, t => {
const remaining = t.filter(todo => !todo.completed).length;
return `${remaining} item${remaining === 1 ? "" : "s"} left`;
}))
),
$.div({ class: "filters" },
collect(
// ... filter buttons
)
),
// Clear completed button - only shows when there are completed todos
when(hasCompletedTodos, {
onTrue: () => $.button(
{
class: "clear-completed",
onClick: () => clearCompleted(),
},
$.of("Clear completed")
),
}),
)
),
Add Styling
.clear-completed {
float: right;
background: none;
border: none;
cursor: pointer;
color: #777;
}
.clear-completed:hover {
text-decoration: underline;
}
Empty State
Let’s also show a message when there are no todos at all:
const hasTodos = Readable.map(todos, t => t.length > 0);
// Wrap the main section
when(hasTodos, {
onTrue: () => $.main({ class: "main" },
each(filteredTodos, {
container: () => $.ul({ class: "todo-list" }),
key: (todo) => todo.id,
render: (todo) => TodoItem({ todo, onToggle: toggleTodo }),
})
),
),
onFalse: () => $.p({ class: "empty-state" }, $.of("No todos yet. Add one above!")),
}),
Add the empty state styling:
.empty-state {
text-align: center;
color: #999;
padding: 20px;
}
Hiding the Footer When Empty
The footer should probably also hide when there are no todos:
when(hasTodos, {
onTrue: () => $.footer({ class: "footer" },
collect(
// ... footer content
)
),
}),
The Complete App Structure
Here’s how the main structure looks now:
return yield* $.div({ class: "todo-app" },
collect(
// Header (always shown)
$.header({ class: "header" },
collect(
$.h1({}, $.of("todos")),
$.input({
class: "new-todo",
placeholder: "What needs to be done?",
autofocus: true,
value: newTodoText,
onInput: (e) => newTodoText.set((e.target as HTMLInputElement).value),
onKeyDown: (e) => {
if (e.key === "Enter") return addTodo();
return Effect.void;
},
}),
)
),
// Main section (only when todos exist)
when(hasTodos, {
onTrue: () => $.main({ class: "main" },
each(filteredTodos, {
container: () => $.ul({ class: "todo-list" }),
key: (todo) => todo.id,
render: (todo) => TodoItem({ todo, onToggle: toggleTodo }),
})
),
),
onFalse: () => $.p({ class: "empty-state" }, $.of("No todos yet. Add one above!")),
}),
// Footer (only when todos exist)
when(hasTodos, {
onTrue: () => $.footer({ class: "footer" },
collect(
$.span(
{ class: "todo-count" },
$.of(Readable.map(todos, t => {
const remaining = t.filter(todo => !todo.completed).length;
return `${remaining} item${remaining === 1 ? "" : "s"} left`;
}))
),
$.div({ class: "filters" },
collect(
// Filter buttons...
)
),
when(hasCompletedTodos, {
onTrue: () => $.button(
{ class: "clear-completed", onClick: () => clearCompleted() },
$.of("Clear completed")
),
}),
)
),
}),
)
);
when vs Conditional Classes
You might wonder when to use when vs just toggling CSS:
Use when |
Use CSS/classes |
|---|---|
| Element shouldn’t exist in DOM | Element should exist but be hidden |
| Has setup/cleanup logic | Simple show/hide |
| Saves memory when hidden | Needs to preserve state |
For our “Clear Completed” button, when makes sense—there’s no reason to have an invisible button in the DOM.
Animations with when
You can add enter/exit animations to when:
when(condition, {
onTrue: () => $.div({}, $.of("Animated!")),
animate: {
enter: "fade-in",
exit: "fade-out",
},
})
We won’t cover animations in depth here, but know that Effex supports CSS-based animations for conditional elements.
Key Takeaways
whenconditionally renders elements- Pass a
Readable<boolean>as the condition onTruerenders when true,onFalsewhen false- Elements are added/removed from DOM, not just hidden
- Use
whenfor presence, CSS for visibility