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.
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
The above command will bootstrap the application and install React dependencies. Once it completes, navigate to the folder:
cd todo-app
And install App's digest:
npm install apps-digest
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;
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;
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 };
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 };
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 };
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.value; // publish the new state this.todos.value = [ ...currentTodos, { id: nanoid(), // we're using a nanoid for ID generation text, done: false, createdAt: +new Date() } ]; } }
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 useStore
. We will also use useState
to control de text input value.
import { useState } from "react"; import { useStore } from "apps-digest"; import TodoStore from "../stores/TodoStore"; const TodoAdd = () => { // Get our store const todoStore = useStore(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> ); };
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 useValue
.
import { useStore, useValue } from "apps-digest"; import TodoStore from "../stores/TodoStore"; const TodoList = () => { // Get our store const todoStore = useStore(TodoStore); // Access the todos value const todos = useValue(todoStore.todos); return ( <div> {todos.map((todo) => ( <TodoItem key={todo.id} {...todo} /> ))} </div> ); };
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> ); }
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.value; // find the TODO to update const todo = currentTodos.find((todo) => todo.id === id); this.todos.value = [ // filter out the other TODOs ...currentTodos.filter((todo) => todo.id !== id), // To produce a new state (always remember to publish new state) { ...todo, text } ]; } }
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.value; const todo = currentTodos.find((todo) => todo.id === id); this.todos.value = [ ...currentTodos.filter((todo) => todo.id !== id), { ...todo, done: !todo.done } ]; } }
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.value; this.todos.value = currentTodos.filter((todo) => todo.id !== id); } }
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 useStore
to access our store from React components.
import { useStore } from "apps-digest"; import TodoStore, { Todo } from "../stores/TodoStore"; const TodoItem = ({ id, text, done }: Todo) => { const todoStore = useStore(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> ); };
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)); }); // ... }
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 = useStore(TodoStore); // Access the new computed orderedTodos value const todos = useValue(todoStore.orderedTodos); return ( <div> {todos.map((todo) => ( <TodoItem key={todo.id} {...todo} /> ))} </div> ); };
And that's it! Our TODOs will stay in order in which they were created, no matter the updates.
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.