Appearance
Complete Example: Todo‑List Application
A live example of the code below is available here.
This example walks through building a fully‑featured Todo‑list app with scalux, showcasing:
- Fetching data from an (emulated) API
- State management (add, toggle, delete tasks)
- Storing todos in a dictionary (object)
- Using the
resolve/updatespattern ofUpdaterfor complex mutations (add/delete) - Implementing history (undo/redo) with
Undoableand built‑in thunks - Separating UI state from business data with
Slice - Accessing data with
Resolverand the built‑inselectors
Folder layout
src/
├── api.ts # Fake external API
├── types.ts # Shared TypeScript types
├── TodoListComponent.tsx # React presentational component (UI)
├── state.ts # scalux state logic (Slices, Updaters, Component …)
├── store.ts # Redux store configuration
├── main.tsx # React entry point
└── index.css # Styles (optional)1. src/types.ts – Shared types
Clear types are essential with TypeScript, so we keep them in one place.
typescript
// src/types.ts
/** Base type for a Todo item */
export type Todo = {
id: string;
label: string;
done: boolean;
};
/** Dictionary form { id → Todo } */
export type TodoDict = Record<string, Todo>;
// ---- Props for the React component ----
/** Props that carry data to display */
export type TodoListComponentDataProps = {
todos: Todo[]; // Todos as an array for rendering
newItemText: string; // Text in the add‑item field
isLoading: boolean; // API loading flag
error: string | null; // API error message
canUndo: boolean; // true if an undo is possible
canRedo: boolean; // true if a redo is possible
};
/** Props that expose handlers the UI can call */
export type TodoListComponentHandlerProps = {
fetchTodos: () => void;
addTodo: (label: string) => void;
toggleTodo: (id: string) => void;
deleteTodo: (id: string) => void;
setNewItemText: (text: string) => void;
undoAction: () => void;
redoAction: () => void;
};
/** Full prop set for the TodoListComponent */
export type TodoListComponentProps = TodoListComponentDataProps &
TodoListComponentHandlerProps;2. src/api.ts – Fake API
A lightweight simulation of a backend.
typescript
// src/api.ts
// Shape returned by the API (may differ from our internal state)
export type ApiTodo = {
id: string;
title: string;
completed: boolean;
};
/** Simulate an API request that fetches initial todos */
export const fetchTodosApi = async (): Promise<ApiTodo[]> => {
console.log("API: Fetching todos …");
await new Promise((r) => setTimeout(r, 800)); // Simulated latency
if (Math.random() > 0.15) {
// 85 % success – otherwise throw
console.log("API: Fetch successful");
const todos: ApiTodo[] = [
{ id: "1", title: "Learn scalux", completed: true },
{ id: "2", title: "Use Updater with resolve/reducers", completed: false },
{ id: "3", title: "Implement undo/redo", completed: false },
{ id: "4", title: "Drink a coffee", completed: true },
{ id: "5", title: "Test deletion", completed: false },
];
return todos;
}
console.error("API: Fetch failed");
throw new Error("Simulated network error while fetching todos.");
};
/** Simulate server‑side todo creation */
export const addTodoApi = async (label: string): Promise<{ id: string }> => {
console.log(`API: Adding todo \"${label}\" …`);
await new Promise((r) => setTimeout(r, 300));
const newId = `temp_${Date.now()}`;
console.log(`API: Todo added with temp ID ${newId}`);
return { id: newId };
};3. src/TodoListComponent.tsx – Presentational component
A vanilla React component responsible only for rendering and delegating interactions to the handlers passed in via props.
tsx
// src/TodoListComponent.tsx
import React, { useEffect } from "react";
import type { TodoListComponentProps } from "./types";
/** Pure React component that displays the Todo list */
export const TodoListComponentDisplay: React.FC<TodoListComponentProps> = ({
// Data props
todos,
newItemText,
isLoading,
error,
canUndo,
canRedo,
// Handler props
fetchTodos,
addTodo,
toggleTodo,
deleteTodo,
setNewItemText,
undoAction,
redoAction,
}) => {
// Load todos on first mount
useEffect(() => {
fetchTodos();
}, [fetchTodos]);
/** Handle form submission */
const handleAdd = (e: React.FormEvent) => {
e.preventDefault();
if (newItemText.trim()) {
addTodo(newItemText.trim());
}
};
return (
<div style={{ padding: "0 2.5em" }}>
<h1>My Scalux Todo List</h1>
{/* Undo / Redo buttons */}
<div>
<button onClick={undoAction} disabled={!canUndo || isLoading}>
Undo
</button>
<button onClick={redoAction} disabled={!canRedo || isLoading}>
Redo
</button>
</div>
{/* Add form */}
<form onSubmit={handleAdd} style={{ margin: "1em 0" }}>
<input
type="text"
placeholder="New task …"
value={newItemText}
onChange={(e) => setNewItemText(e.target.value)}
disabled={isLoading}
aria-label="New task"
/>
<button type="submit" disabled={isLoading || !newItemText.trim()}>
Add
</button>
</form>
{/* Loading indicator */}
{isLoading && <p>Loading todos …</p>}
{/* Error */}
{error && <p style={{ color: "red" }}>Error: {error}</p>}
{/* Todo list */}
{!isLoading && !error && (
<ul style={{ listStyle: "none", padding: 0 }}>
{todos.map((todo) => (
<li
key={todo.id}
style={{
display: "flex",
alignItems: "center",
margin: "0.5em 0",
textDecoration: todo.done ? "line-through" : "none",
opacity: todo.done ? 0.6 : 1,
}}
>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
style={{ marginRight: "0.5em" }}
/>
<span style={{ flexGrow: 1 }}>{todo.label}</span>
<button
onClick={() => deleteTodo(todo.id)}
style={{ marginLeft: "0.5em" }}
>
Delete
</button>
</li>
))}
</ul>
)}
{/* Empty state */}
{!isLoading && !error && todos.length === 0 && <p>No tasks yet!</p>}
</div>
);
};4. src/state.ts – scalux state logic
Here we define the state shape, the update logic (Updaters), and connect the React component.
typescript
// src/state.ts
import { State, Slice, Undoable } from "scalux";
import { fetchTodosApi, addTodoApi } from "./api";
import type { Todo, TodoDict, TodoListComponentDataProps } from "./types";
import { TodoListComponentDisplay } from "./TodoListComponent";
// ---- 1. State structure ----
// Business data slice (todos, input text) – wrapped in Undoable for history
const dataSlice = Slice(
Undoable({
todos: {} as TodoDict,
newItemText: "",
})
);
// UI slice (loading / error)
const uiSlice = Slice({
loading: false,
error: null as string | null,
});
// ---- 2. Initialize scalux ----
const { Component, register, Updater, Resolver, selectors, undo, redo } = State(
{
data: dataSlice,
ui: uiSlice,
}
);
// ---- 3. Updaters ----
/** Update the new‑item text field */
const setNewItemText = Updater((_s, txt: string) => ({
data: { newItemText: txt },
}));
/** Toggle a todo */
const toggleTodo = Updater((state, id: string) => {
const current = selectors.pick.data.todos(state)[id];
if (!current) return {};
return { data: { todos: { [id]: { ...current, done: !current.done } } } };
});
/** Fetch todos from the API */
const fetchTodos = Updater(async () => {
try {
const apiTodos = await fetchTodosApi();
const dict = apiTodos.reduce((acc, t) => {
acc[t.id] = { id: t.id, label: t.title, done: t.completed };
return acc;
}, {} as TodoDict);
return { data: { todos: dict }, ui: { loading: false, error: null } };
} catch (e: any) {
return { ui: { loading: false, error: e.message || "Unknown error" } };
}
});
/** Add a new todo (resolve / updates pattern) */
const addTodo = Updater({
resolve: Resolver(async (_s, label: string) => {
if (!label.trim()) throw new Error("Label cannot be empty");
const { id } = await addTodoApi(label);
return { id, label, done: false } as Todo;
}),
updates: {
data: (draft, todo) => {
draft.todos[todo.id] = todo;
draft.newItemText = "";
},
},
});
/** Delete a todo */
const deleteTodo = Updater({
resolve: Resolver((_s, id: string) => id),
updates: {
data: (draft, id) => {
delete draft.todos[id];
},
},
});
// ---- 4. Connect the React component ----
const selectTodoListData = Resolver((state): TodoListComponentDataProps => {
const todosDict = selectors.pick.data.todos(state);
const newItemText = selectors.pick.data.newItemText(state);
const isLoading = selectors.pick.ui.loading(state);
const error = selectors.pick.ui.error(state);
const raw = selectors.rawGrab.data(state);
const canUndo = raw.past.length > 0;
const canRedo = raw.future.length > 0;
return {
todos: Object.values(todosDict),
newItemText,
isLoading,
error,
canUndo,
canRedo,
};
});
export const ConnectedTodoList = Component({
domain: "TodoList",
render: TodoListComponentDisplay,
data: selectTodoListData,
handlers: {
fetchTodos,
addTodo,
toggleTodo,
deleteTodo,
setNewItemText,
undoAction: undo,
redoAction: redo,
},
});
// ---- 5. Register reducers ----
const { reducer } = register();
export { reducer };5. src/store.ts – Store configuration
typescript
// src/store.ts
import { configureStore } from "scalux";
import { reducer } from "./state";
export const store = configureStore({ reducer });
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;6. src/main.tsx – Application entry point
tsx
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "scalux"; // React‑Redux provider
import { store } from "./store";
import { ConnectedTodoList } from "./state";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Provider store={store}>
<ConnectedTodoList />
</Provider>
</React.StrictMode>
);This code delivers a functional, well‑structured Todo‑list app that leverages all the key features of scalux: reduced boilerplate, async state handling, undo/redo, and the orchestra of Slice, Updater, Resolver, Component, and selectors. Feel free to experiment and extend!
