Create a To Do App with React, Recoil and TypeScript
First, a little about Recoil
Recoil is a state management library developed by Facebook, initially used by Facebook itself, but now made open source. According to its official website, recoiljs.org, these are its 3 distinct features:
- Minimal and Reactish: Use a flexible, shared React-like state.
- Data-flow graph: “Derived data and asynchronous queries are tamed with pure functions and efficient subscriptions.”
- Cross-App observation: “Implement persistence, routing, time-travel debugging, or undo by observing all state changes across your app, without impairing code-splitting.”
You can take a look at its documentation here.
Show me some code!
Let’s start building the application. We will be using vite.
pnpm create vite .
Choose React with TypeScript and follow all instructions that appear in your terminal application.
If we have no errors, we are good to go. Now, it’s time to install Recoil. In your terminal, run:
pnpm add recoil
Creating the components
First, delete the return statement from the App.tsx component and make replace it with with return null. We will be working with this component in a while.
It is a common practice in Typescript, that you must define your types before being able to start working on the real application. For that matter, create a types.d.ts file in your src directory. Start by filling this in:
// types.d.ts
export interface TodoContent {
id: string;
title: string;
description: string;
}
Now create a new folder in the src directory called “components”. Here is where all the Todo components will be created. Create a new file called TodoItem.tsx in this folder. We will initialize it like this:
// TodoItem.tsx
import { TodoContent } from "../types";
export function TodoItem(props: TodoContent) {
return <div></div>;
}
Create another file called TodoList.tsx in the same folder and initialize it like this:
// TodoList.tsx
export function TodoList() {
return <div></div>;
}
It’s high time we start creating Recoil State.
Finally, some state
Head over to your index.tsx file and wrap your App component in the RecoilRoot component.
import ReactDOM from "react-dom";
import App from "./App";
import { RecoilRoot } from "recoil";
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById("root")
);
In your src directory, create a new folder “state” and add a new file todoState.ts.
In recoil, we define state using a function called atom. This is what it looks like:
// todoState.ts
import { atom } from "recoil";
import { TodoContent } from "../types";
export const todoContentState = atom<TodoContent[]>({
key: "todoContents",
default: [],
});
key is a unique identifier for the todoContentState and default is simply the default state. With TypeScript, you can also add the type of state between angle brackets.
Go ahead and add this too:
// todoState.ts
...
export const todoCompleteState = atomFamily<boolean, string>({
key: "todoCompleteState",
default: false,
});
atomFamily returns a function to which you pass a unique ID, enabling a unique state for your component.
This piece of state is for each individual Todo item; it does not need to be fed, or updated by passing props, so improves performance drastically by preventing unnecessary rerenders. We store static data (data that is very unlikely to change in a single piece of state, like todoContentState). Let’s see this in action by editing our TodoItem component.
// TodoItem.tsx
import { useRecoilState, useSetRecoilState } from "recoil";
import { todoCompleteState, todoContentState } from "../state/todoState";
import { TodoContent } from "../types";
export function TodoItem(props: TodoContent) {
const { description, title, id } = props;
const [isComplete, setIsComplete] =
useRecoilState(todoCompleteState(props.id));
const setTodos = useSetRecoilState(todoContentState);
const toggleComplete = () => setIsComplete(prevState =>
!prevState);
const deleteTodo = () => setTodos(todos => todos.filter(todo =>
todo.id !== id));
return (
<div>
<h2>{title}</h2>
{description && <p>{description}</p>}
<div>
<button onClick={toggleComplete}>
{isComplete ? "Not complete" : "Complete"}
</button>
<button onClick={deleteTodo}>Delete</button>
</div>
</div>
);
}
Notice the use of the hook useRecoilState to fetch the state specific to this ID. So, this causes only this component to re-render when we change its recoil state. Another reason this happens is because the child component’s state is not stored in the parent’s state.
Just before we continue, your project directory should look something like this:
[TODO]
We can now add recoil state to our TodoList component, that will render all the todo items.
// TodoList.tsx
import { useRecoilValue } from "recoil";
import { todoContentState } from "../state/todoState";
import { TodoItem } from "./TodoItem";
export function TodoList() {
const todos = useRecoilValue(todoContentState);
return (
<div>
{todos.map(todoProps => (
<TodoItem {...todoProps}
key={todoProps.id}
/>
))}
</div>
);
}
Now the client needs a component to actually add a todo. We create a new component called AddTodo in a file called AddTodo.tsx. Before this though, we must install a JS module called nanoid to help with generating unique IDs for each todo.
pnpm add nanoid.
This is what your component should look like:
import {
ChangeEventHandler,
FormEventHandler,
useState
} from "react";
import { useSetRecoilState } from "recoil";
import { todoContentState } from "../state/todoState";
import { nanoid } from "nanoid";
import { TodoContent } from "../types";
export function AddTodo() {
const [content, setContent] = useState<Omit<TodoContent, "id">>
({ description: "", title: "" });
const handleChange: ChangeEventHandler<HTMLInputElement> = e =>
setContent(prev => ({ ...prev, [e.target.id]: e.target.value
}));
const setTodos = useSetRecoilState(todoContentState);
const addTodo: FormEventHandler<HTMLFormElement> = e => {
e.preventDefault();
setTodos(todos => [...todos, { ...content, id: nanoid() }]);
};
return (
<form onSubmit={addTodo}>
<input onChange={handleChange} value={content.title}
id="title" required />
<input onChange={handleChange}
value={content.description} id="description" />
<button type="submit" disabled={!content.title}>
Add Todo
</button>
</form>
);
}
Final compilation
All the necessary components are now built. Time to add them to the App component.
// App.tsx
import { AddTodo } from "./components/AddTodo";
import { TodoList } from "./components/TodoList";
const App = () => {
return (
<div>
<AddTodo />
<TodoList />
</div>
);
};
export default App;
The app is complete. Yay!
Soon, I’ll also the adding the GitHub repo link in an update.