Todo App Tutorial
Welcome to a short 5min tutorial on using Nalanda.
What are we building?
In this tutorial, we'll craft a Todo app to demonstrate how Nalanda simplifies state management.
Below is a preview of the final product:
import { createStore } from "@nalanda/core"; import { StoreProvider } from "@nalanda/react"; import { todoSlice } from "./todo-slice"; import { Todo } from "./Todo"; // Establish a global store incorporating your slices. const store = createStore({ slices: [todoSlice] }); export default function App() { return ( <StoreProvider store={store}> <div className="App"> <Todo /> </div> </StoreProvider> ); }
1. The Slice and State
Before diving in, ensure you've familiarized yourself with the Getting Started guide.
As our focus is a Todo app, let's start by defining the data structure for our todos:
import { createKey } from "@nalanda/react";
// Initialize a key to configure our slice
const key = createKey("todoSlice", []);
// Todos can have one of two statuses
type Status = "pending" | "completed";
// We'll use this filter to sift through the todos
export type Filter = Status | "all";
type Todo = {
description: string;
id: number;
status: Status;
};
State Fields
Think of State Fields as the foundational elements of your slice state. They can be kept internal or shared across your application via a Slice (more on this later).
const filterField = key.field<Filter>("all");
const todosField = key.field<Todo[]>([]);
We'll also create a Derived Field that calculates its value based on other fields or slices.
const filteredTodosField = key.derive((state) => {
const filter = filterField.get(state);
const todos = todosField.get(state);
if (filter === "all") {
return todos;
}
return todos.filter((todo) => todo.status === filter);
});
Defining the Slice
Slices are central to managing your app's state. They wrap up your exposed fields and actions into a single object.
export const todoSlice = key.slice({
// Specify the fields for external access
filteredTodos: filteredTodosField,
filter: filterField,
});
Take note: we intentionally omitted todosField in key.slice({ })
.
By doing so, we ensure it remains confined internally to our slice.
Such a strategy minimizes the externally accessible state, promoting a more robust and scalable architecture.
Whats the difference between a Field and a Slice?
-
A Field is a single piece of state that can be accessed and modified in a file.
-
A Slice is a collection of fields and actions that can be accessed and modified outside of the file.
You will always be importing a Slice into your React components, never a field!
2. The Actions
Actions dictate the modifications we can make to the slice.
Defining Actions
We start by defining an action that alters the filterField
.
Recall that filterField
is used by filteredTodosField
to parse through the todos.
function changeFilterType(filterType: Filter) {
return filterField.update(filterType);
}
Following that, we'll establish an action to toggle a Todo's status between 'completed' and 'pending'. We employ a different update API similar to that of React's setState -- we pass a callback that receives the current value and expects the new value to be returned.
function markComplete(id: number) {
return todosField.update((todos) => {
return todos.map((todo) => {
if (todo.id === id) {
return {
...todo,
status: todo.status === "completed" ? "pending" : "completed"
};
}
return todo;
});
});
}
Once defined, these actions can be incorporated into the slice, making them available for external invocation:
export const todoSlice = key.slice({
// Specify the fields for external access
filteredTodos: filteredTodosField,
filter: filterField,
// Specify the actions for external access
changeFilterType,
markComplete,
});
3. The React component
Nalanda strives to streamline state management, ensuring independence from specific UI frameworks. To reap benefits, keep your React components slim and keep as much state logic in the slice.
We will now show how to read exposed data from a slice in a React component.
function Filters() {
// useTrack helps re-render the component when the tracked fields change
const { filter } = useTrack(todoSlice);
const store = useStore();
return (
<div>
{FILTER_TYPES.map((filterType) => {
return (
<React.Fragment key={filterType}>
<input
name="filter"
type="radio"
value={filterType}
checked={filter === filterType}
/>
<label>{filterType}</label>
</React.Fragment>
);
})}
</div>
);
}
Dispatching an Action
We will write a handler to dispatch the changeFilterType
action when the user selects a filter:
function Filters() {
const { filter } = useTrack(todoSlice);
const store = useStore();
return (
<div>
{FILTER_TYPES.map((filterType) => {
return (
<React.Fragment key={filterType}>
<input
name="filter"
type="radio"
value={filterType}
checked={filter === filterType}
onChange={() => {
// dispatch the action to the store
store.dispatch(todoSlice.changeFilterType(filterType));
}}
/>
<label>{filterType}</label>
</React.Fragment>
);
})}
</div>
);
}
4. Wiring the Store
We need to setup the store and make use provider to make available to all our React component:
import { createStore } from "@nalanda/core";
import { StoreProvider } from "@nalanda/react";
import { todoSlice } from "./todo-slice";
import { Todo } from "./Todo";
// Establish a global store incorporating all slices.
const store = createStore({
slices: [todoSlice]
});
export default function App() {
return (
<StoreProvider store={store}>
<div className="App">
<Todo />
</div>
</StoreProvider>
);
}
5. The Final Product
Here is the final product, feel free to play around with it:
import { createStore } from "@nalanda/core"; import { StoreProvider } from "@nalanda/react"; import { todoSlice } from "./todo-slice"; import { Todo } from "./Todo"; // Establish a global store incorporating your slices. const store = createStore({ slices: [todoSlice] }); export default function App() { return ( <StoreProvider store={store}> <div className="App"> <Todo /> </div> </StoreProvider> ); }