DEV Community

Marty Roque
Marty Roque

Posted on • Edited on • Originally published at blog.emrock.net

State Management in React With App’s Digest

When the complexity of an application is such that local component state is not feasible, we need to embrace some form of state management that fits our architecture and allows our application to scale while preserving a good code readability.

Redux provides a good scalability, unfortunately it comes at the cost of too much code boilerplate. This has created a good opportunity for other simple state management libraries, such as Recoil, Zustand, Jōtai and App's Digest to thrive.

App's Digest has the smallest bundle size and offers an API that allows your code to be decoupled, portable, and testable, regardless of the UI framework your application uses.

This guide will give you a comprehensive look at the App's Digest library and prepare you to use it in your next project.

How App's Digest works

App's Digest is an atomic state management. Unlike Redux and Zustand, where data is stored in global storage outside the component tree. In App's Digest, data is stored in centralized locations called stores, with units of state called values that your application can subscribe to.

how apps digest works

App's Digest stores data in small, independent and updatable values that combine to form a more complex values (computed values), and it only triggers strictly-needed, isolated updates for computations (e.g. React components) subscribed to values. All this without the need to create a large number of providers or use derived states selectors. Instead, App's Digest gives you access to a particular state anywhere in your application and manipulating it to your preference.

Building an app using App's Digest

Let’s build a TODO app where you will learn how to create, update, and delete tasks.

Create a new React app

Let's use the create-react-app library to create a new React application. From the terminal, we will run:

npx create-react-app todo-app
Enter fullscreen mode Exit fullscreen mode

The above command will bootstrap the application and install React dependencies. Once it completes, navigate to the folder:

cd todo-app
Enter fullscreen mode Exit fullscreen mode

And install App's digest:

npm install apps-digest
Enter fullscreen mode Exit fullscreen mode

Creating a Store

Now let's create a basic representation of the store using a class. You can place all your stores in src/stores. We will name this file TodoStore.ts.

import { AppsDigestStore } from "apps-digest";

class TodoStore extends AppsDigestStore {
}

export default TodoStore;
Enter fullscreen mode Exit fullscreen mode

Creating Store Values

Our store needs values it can update and keep track of. So, let's import AppsDigestValue and create our first store value, our TODOs:

import { AppsDigestStore, AppsDigestValue } from "apps-digest";

export type Todo = {
  id: string;
  text: string;
  done: boolean;
  createdAt: number;
};

export type TodoList = Todo[];

class TodoStore extends AppsDigestStore {
  // our store value with initial value as empty array
  public todos = new AppsDigestValue<TodoList>([]);
}

export default TodoStore;
Enter fullscreen mode Exit fullscreen mode

Creating our components

Create a components folder in the src folder.

Add TODO

In the components folder, create a file called TodoAdd.tsx with an input and a button. This component is where our users will add new TODOs.

const TodoAdd = () => {
  return (
    <div>
      <input placeholder="New TODO" />
      <button>Add</button>
    </div>
  );
};

export { TodoAdd };
Enter fullscreen mode Exit fullscreen mode

TODO Item

Now, let's create a TodoItem.tsx with a checkbox, an input and a button. This component will render individual TODOs.

const TodoItem = () => {
  return (
    <div>
      <input type="checkbox" />
      <input />
      <button>Delete</button>
    </div>
  );
};

export { TodoItem };
Enter fullscreen mode Exit fullscreen mode

TODO List

Next, we need to create a component that will display all our TODOs. In the components folder, create another TSX file called TodoList.tsx and render the list of todos.

import { TodoItem } from "./TodoItem";

const TodoList = () => {
  const todos = []; // Empty for now
  return (
    <div>
      {todos.map((todo) => (
        <TodoItem key={todo.id} {...todo} />
      ))}
    </div>
  );
};

export { TodoList };
Enter fullscreen mode Exit fullscreen mode

Adding TODOs

Now, let's create our addTodo setter method to our store. Setter methods in a Store are optional, but it is a good practice to follow the single-responsibility pattern, which ensures a single source of truth and update.

class TodoStore extends AppsDigestStore {
  public todos = new AppsDigestValue<TodoList>([]);

  public addTodo(text: string) {
    // get current value from state
    const currentTodos = this.todos.currentValue();

    // publish the new state
    this.todos.publish([
      ...currentTodos,
      {
        id: nanoid(), // we're using a nanoid for ID generation
        text,
        done: false,
        createdAt: +new Date()
      }
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Once our setter is in place, let's consume it in our TodoAdd.tsx component. To get our store in a component, we will use the hook useAppsDigestStore. We will also use useState to control de text input value.

import { useState } from "react";
import { useAppsDigestStore } from "apps-digest";

import TodoStore from "../stores/TodoStore";

const TodoAdd = () => {
  // Get our store
  const todoStore = useAppsDigestStore(TodoStore);

  const [value, setValue] = useState("");

  const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  const onAddClick = () => {
    if (value) {
      // Call our setter
      todoStore.addTodo(value);

      // Reset the input value
      setValue("");
    }
  };

  return (
    <div>
      <input 
        placeholder="New TODO"
        value={value} 
        onChange={onInputChange}
      />
      <button onClick={onAddClick}>Add</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Displaying TODO list

Once the code for adding TODOs is in place, we can now display our TODO list by accessing store values within a component. For this, we will use the hook useAppsDigestValue.

import { useAppsDigestStore, useAppsDigestValue } from "apps-digest";

import TodoStore from "../stores/TodoStore";

const TodoList = () => {
  // Get our store
  const todoStore = useAppsDigestStore(TodoStore);

  // Access the todos value
  const todos = useAppsDigestValue(todoStore.todos);

  return (
    <div>
      {todos.map((todo) => (
        <TodoItem key={todo.id} {...todo} />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Let's give this a quick test by adding the TodoAdd.tsx and the TodoList.tsx components to our App.tsx:

import { TodoAdd } from "./components/TodoAdd";
import { TodoList } from "./components/TodoList";

export default function App() {
  return (
    <div className="App">
      <h1>State Management in React with App's Digest</h1>
      <h2>My TODO list</h2>
      <TodoList />
      <TodoAdd />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

adding todos

Very easy, right? Now, if you'd like, take some time to add some styling. Up next, we will see how to update, toggle and remove TODOs.

Updating TODOs

To allow our users update the text of their TODOs, we will create a new setter method in our TodoStore called updateTodo.

class TodoStore extends AppsDigestStore {
  public todos = new AppsDigestValue<TodoList>([]);

  public addTodo(text: string) {}

  public updateTodo(id: string, text: string) {
    const currentTodos = this.todos.currentValue();
    // find the TODO to update
    const todo = currentTodos.find((todo) => todo.id === id);

    this.todos.publish([
      // filter out the other TODOs
      ...currentTodos.filter((todo) => todo.id !== id),
      // To produce a new state (always remember to publish new state)
      { ...todo, text }
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Toggling TODOs

Let's create another setter method that will toggle a given TODO's done flag. We will name it toggleTodo.

class TodoStore extends AppsDigestStore {
  public todos = new AppsDigestValue<TodoList>([]);

  public addTodo(text: string) {}

  public updateTodo(id: string, text: string) {}

  public toggleTodo(id: string) {
    const currentTodos = this.todos.currentValue();
    const todo = currentTodos.find((todo) => todo.id === id);

    this.todos.publish([
      ...currentTodos.filter((todo) => todo.id !== id),
      { ...todo, done: !todo.done }
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

Removing TODOs

This will be another setter method within our store, removeTodo:

class TodoStore extends AppsDigestStore {
  public todos = new AppsDigestValue<TodoList>([]);

  public addTodo(text: string) {}

  public updateTodo(id: string, text: string) {}

  public toggleTodo(id: string) {}

  public removeTodo(id: string) {
    const currentTodos = this.todos.currentValue();

    this.todos.publish(currentTodos.filter((todo) => todo.id !== id));
  }
}
Enter fullscreen mode Exit fullscreen mode

Putting all together

With all our update, toggle and remove methods created, let's now consume them in our TodoItem.tsx component. Remember, we use useAppsDigestStore to access our store from React components.

import { useAppsDigestStore } from "apps-digest";

import TodoStore, { Todo } from "../stores/TodoStore";

const TodoItem = ({ id, text, done }: Todo) => {
  const todoStore = useAppsDigestStore(TodoStore);

  const onCheckboxChange = () => {
    todoStore.toggleTodo(id);
  };

  const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    todoStore.updateTodo(id, e.target.value);
  };

  const onDeleteClick = () => {
    todoStore.removeTodo(id);
  };

  return (
    <div>
      <input type="checkbox" onChange={onCheckboxChange} checked={done} />
      <input value={text} onChange={onInputChange} />
      <button onClick={onDeleteClick}>Delete</button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Computed Values

Now, as you noticed, the TODO list order changes as you update any given TODO, which is a bit annoying. So let's fix this by using the createdAt property of our TODOs to order them.

To achieve this we will use a great App's Digest feature, computed values, which allows us to consume store values, compute them in a callback and produce a single result.

So, let's create our orderedTodos computed value.

class TodoStore extends AppsDigestStore {
  public todos = new AppsDigestValue<TodoList>([]);
  public orderedTodos = this.computedValue([this.todos], (todos) => {
    return todos.sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1));
  });

  // ...
}
Enter fullscreen mode Exit fullscreen mode

computedValue method accepts an array of store values, and a callback we expect to receive these to be computed or derived.

Cool, let's use our new computed value in our TODO list component.

const TodoList = () => {
  // Get our store
  const todoStore = useAppsDigestStore(TodoStore);

  // Access the new computed orderedTodos value
  const todos = useAppsDigestValue(todoStore.orderedTodos);

  return (
    <div>
      {todos.map((todo) => (
        <TodoItem key={todo.id} {...todo} />
      ))}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

And that's it! Our TODOs will stay in order in which they were created, no matter the updates.

updating todos

Conclusion

The App's Digest atomic and zero-provider approach to state management is newer than Redux and Zustand, but it has been well-received by the developer community. App's Digest has proven reliable in small to large-size projects, and it’s without a doubt that it is an equal competitor to any state management library out there.

Resources

Final TODO app code
App's Digest Github
App's Digest NPM Package

Top comments (0)