Documentation
Tutorial: Todo (5min)

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:

todo-slice.ts
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).

todo-slice.ts
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.

todo-slice.ts
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.

todo-slice.ts
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.

todo-slice.ts
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.

todo-slice.ts
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:

todo-slice.ts
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.

Todo.tsx
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:

Todo.tsx
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:

index.tsx
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>
  );
}