Skip to main content

Creating the state

Our Todo app state will be composed of 3 data structures, named as follows:

  • TodoItem represents a single todo item
  • TodoList represents a list of TodoItems
  • State is the store state, which contain the TodoList

These can be plain JavaScript objects, but also ES6 classes.

I'll use classes in this page, but will also show the code with plain objects at the end of this tutorial.

TodoItem

The TodoItem class represents a single todo item, with some text and a completed status, which starts as false (not completed):

TodoItem.ts
export class TodoItem {
constructor(
public text: string,
public completed: boolean = false) {
}
}

We'll add to it a toggleCompleted() function, which returns a copy of the item with the same text, but opposite completed status:

TodoItem.ts
export class TodoItem {
constructor(
public text: string,
public completed: boolean = false) {
}

toggleCompleted() {
return new TodoItem(this.text, !this.completed);
}
}

This class is immutable, as it doesn't have any setters, and its single function toggleCompleted returns a new TodoItem object, instead of modifying the current one.

TodoList

The TodoList class is a simple list of todo items of type TodoItem:

TodoList.ts
export class TodoList {  
constructor(public readonly items: TodoItem[] = []) {}
}

We can add of sorts of functions to the TodoList class, which will later help us manage the list of todos. These are a few examples:

  • addTodoFromText - Add a new todo item to the list from a text string.
  • addTodo - Add a new todo item to the list.
  • ifExists - Check if a todo item with a given text already exists.
  • removeTodo - Remove a todo item from the list.
  • toggleTodo - Toggle the completed status of a todo item.
  • isEmpty - Check if there are no todos that appear when a filter is applied.
  • iterator - Allow iterating over the list of todos.
  • toString - Return a string representation of the list of todos.
  • empty - A static empty list of todos.

Here is the full code of the TodoList class, with all the above functions implemented:

TodoList.ts
export class TodoList {  
constructor(public readonly items: TodoItem[] = []) {}

addTodoFromText(text: string): TodoList {
const trimmedText = text.trim();
const capitalizedText = trimmedText.charAt(0).toUpperCase() + trimmedText.slice(1);
return this.addTodo(new TodoItem(capitalizedText));
}

addTodo(newItem: TodoItem): TodoList {
if ((newItem.text === '') || this.ifExists(newItem.text))
return this;
else
return new TodoList([newItem, ...this.items]);
}

ifExists(text: string): boolean {
return this.items.some((todo) => todo.text.toLowerCase() === text.toLowerCase());
}

removeTodo(item: TodoItem): TodoList {
return new TodoList(this.items.filter(itemInList => itemInList !== item));
}

toggleTodo(item: TodoItem): TodoList {
const newTodos = this.items.map(itemInList => (itemInList === item) ? item.toggleCompleted() : itemInList);
return new TodoList(newTodos);
}

isEmpty() {
return this.items.length === 0;
}

* [Symbol.iterator]() {
for (let i = 0; i < this.items.length; i++) {
yield this.items[i];
}
}

toString() { return `TodoList{${this.items.join(',')}}`; }

static empty: TodoList = new TodoList();
}

Note again that all functions above return new TodoList objects, instead of modifying the current one. This means TodoList is immutable.

Also note that all these functions are easy to create, and it would also be easy to create unit tests for them.

Adding these functions to the TodoList class will allow us to manage the immutable list of todos in a clean and efficient way, without resorting to external "immutable state libraries" like Immer.

State

Finally, we need to define the store state. In the future, we may want to add a lot of different things to the state, but for now we'll keep it simple and just add the TodoList to it:

State.ts
export class State {
todoList: TodoList;

constructor({ todoList }: { todoList: TodoList }) {
this.todoList = todoList;
}

withTodoList(todoList: TodoList): State {
return new State({ todoList: todoList });
}

static initialState: State = new State({ todoList: TodoList.empty });
}

Note the state class above has a withTodoList() function that returns a copy of the state, but replacing the current list of todos with a new one. This is an immutable operation, as it creates a new state object.

We also defined a static variable called initialState. That's optional, but common. It's just a default state that can be used when the store is created. For example, instead of:

const store = createStore<State>({
initialState: new State({todoList: TodoList.empty}),
});

We can now write:

const store = createStore<State>({
initialState: State.initialState,
});