Skip to content

Tutorial Todo App

In this tutorial we'll cover how to handle building full (but simple) app and how to handle exome nested stores with react. This is not tied to react specifically, just used as an example (practically can be any other framework).

Lets start with creating simple todo store for single todo item. Instead of thinking how can you update todo items, think of each store piece as separate app.

todo.store.ts
import { Exome } from "exome"
 
export class TodoStore extends Exome {
  public completed = false
 
  constructor(public content: string) {
    super()
  }
 
  public toggle() {
    this.completed = !this.completed
  }
}

We created completed property with default value false.

However content we want to define every time we create a new todo so we'll do that in constructor.

And we need some way to update completed status so that will be handled in toggle action.

Next we need some store that handles list of todos:

todo-list.store.ts
import { Exome } from "exome"
import { TodoStore } from "./todo.store"
 
export class TodoListStore extends Exome {
  public todos: TodoStore[] = []
 
  public addTodo(todo: TodoStore) {
    this.todos.push(todo)
  }
}

Here we have a store (TodoListStore) that contains other stores (TodoStore) in todos property. This is what exome was designed for.

Also we need a way to add new todo to the list, so that will be handled by addTodo action. This action expects already created TodoStore instance, but that is not a must, action can also create new TodoStore instance itself! I created it this way just to give extra control to dev creating a new todo item.

Ok so that's basically it, we created all that is needed for this app to work. Now lets create UI for this app.

I'll use react for this example, but you can chose any other integration.

app.tsx
import { getExomeId } from "exome"
import { useStore } from "exome/react"
import { TodoListStore } from "./todo-list.store"
import { TodoStore } from "./todo.store"
 
const todoListStore = new TodoListStore()
 
// Fill store with some demo values
todoListStore.addTodo(new TodoStore("Take out trash"))
todoListStore.addTodo(new TodoStore("Work out"))
 
function Todo({ todo }: { todo: TodoStore }) {
  const { completed, content, toggle } = useStore(todo)
 
  return (
    <label>
      <input
        type="checkbox"
        onChange={toggle}
        checked={completed}
      />
      {content}
    </label>
  );
}
 
function TodoList() {
  const { todos } = useStore(todoListStore)
 
  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={getExomeId(todo)}>
            <Todo todo={todo} />
          </li>
        ))}
      </ul>
    </div>
  );
}
Live preview

Now there's a lot to unpack here.

First of all - there are two components instead of one. This is due to nature of exome. Since we have two different stores each will have it's own update action.

For example addTodo action will only trigger updates in <TodoList> component becuase of useStore hook used there. However toggle action will trigger changes only in <Todo> component.

If we'd have only one component, we'd need to call some other action on TodoListStore (or manually update it update(todoListStore)). This is not prefered as there are no changes and those components don't need to be updated.

Second - react requires every array item to have a key so here we can use getExomeId(todo) to get unique id of each TodoStore instance.

Third - we use onChange={toggle} toggle being action from a class. Every action is sort of a wrapper for the changes inside them so by default they are bound to the class. So there's NO NEED to do this: onChange={() => toggle()}, except if you want to pass some payload to the action.


Now we're gonna create a way to add new todo items to the list.

function TodoList() {
  const { todos } = useStore(todoListStore) 
  const { todos, addTodo } = useStore(todoListStore) 
 
  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={getExomeId(todo)}>
            <Todo todo={todo} />
          </li>
        ))}
      </ul>
      <form
        onSubmit={(e) => { 
          e.preventDefault() 
          const { content: contentEl } = e.target.elements 

          addTodo(new TodoStore(contentEl.value)) 

          contentEl.value = ''
        }}
      >
        <input name="content" />
        <button type="submit">add</button>
      </form>
    </div>
  );
}
Live preview

Now we can trigger updates to <TodoList> component by adding new items to store via action.

We can add filtering functionality to TodoListStore.

class TodoListStore extends Exome {
  public todos: TodoStore[] = []
  public filter: "all" | "todo" | "done" = "all"
 
  public get filteredTodos() { 
    if (this.filter === "done") { 
      return this.todos.filter((t) => t.completed) 
    } 
    if (this.filter === "todo") { 
      return this.todos.filter((t) => !t.completed) 
    } 
    return this.todos 
  } 
 
  public addTodo(todo: TodoStore) {
    this.todos.push(todo)
  }
 
  public setFilter(filter: TodoListStore["filter"]) { 
    this.filter = filter 
  } 
}
function TodoList() {
  const { todos } = useStore(todoListStore)
  const { todos, addTodo } = useStore(todoListStore) 
  const { 
    filteredTodos, 
    addTodo, 
    filter, 
    setFilter, 
  } = useStore(todoListStore) 
 
  return (
    <div>
      <button
        onClick={() => setFilter("all")}
        disabled={filter === "all"}
      >
        all
      </button>
      <button
        onClick={() => setFilter("todo")}
        disabled={filter === "todo"}
      >
        todo
      </button>
      <button
        onClick={() => setFilter("done")}
        disabled={filter === "done"}
      >
        done
      </button>
 
      <ul>
        {todos.map((todo) => ...)}
        {filteredTodos.map((todo) => ...)}
      </ul>
 
      [...]
    </div>
  );
}

Not much new to unpack here, but one thing I want to mention.

For getting filtered results we used getter filteredTodos. Exome doesn't touch getters and setters so they work as usual without update cycles etc.

Still one thing is missing. When switching to todo and updating some of the todo items, it doesn't update list accordingly. That's because there's no action called. We can use onAction effect to fix this.

import { onAction, update } from "exome"
 
class TodoListStore extends Exome {
  public todos: TodoStore[] = []
 
  constructor() { 
    super() 

    onAction(TodoStore, "toggle", () => { 
      update(this) 
    }) 
  } 
 
  public addTodo(todo: TodoStore) {
    this.todos.push(todo)
  }
}
Live preview

Live code playground

Now we have a working todo app. Lets do a bit of a recap of what we learned from this:

  • Working with nested stores;
  • How actions trigger update;
  • How to make use of getters;
  • How to use effects.