DEV Community

Sirwan Afifi
Sirwan Afifi

Posted on • Updated on

Recoil with React and TypeScript

Recoil is yet another state management library for React, I just wanted to give it a try so I decided to re-create my other post's sample (MobX with React and TypeScript
) in Recoil.

Recoil has pretty simple API to work with, the functionality is the same as other state management library but the names are different, Recoil seems to be a bit better in terms of code boilerplate. It has two important concepts to know:

  • Atoms: Atoms are units of state. They're updateable and subscribable: when an atom is updated, each subscribed component is re-rendered with the new value. They can be created at runtime, too. Atoms can be used in place of React local component state. If the same atom is used from multiple components, all those components share their state.

  • Selector: A selector is a pure function that accepts atoms or other selectors as input. When these upstream atoms or selectors are updated, the selector function will be re-evaluated. Components can subscribe to selectors just like atoms, and will then be re-rendered when the selectors change.

Creating a Simple Todo App with Recoil and React

let's create a simple todo app using React:

mkdit recoil_sample && cd recoil_sample
npx create-react-app . --typescript
yarn add bootstrap --save
Enter fullscreen mode Exit fullscreen mode

Now go to index.tsx and import bootstrap.css:

import "bootstrap/dist/css/bootstrap.css"
Enter fullscreen mode Exit fullscreen mode

Now we'll install the needed dependencies:

yarn add recoil uuid @types/uuid --save
Enter fullscreen mode Exit fullscreen mode

Now let's create a store inside the project

import { atom, selector } from "recoil";

export interface Todo {
  id?: string;
  title: string;
  completed: boolean;
}

export const todosState = atom({
  key: "todos",
  default: [] as Todo[],
});

export const infoValue = selector({
  key: "infoValue",
  get: ({ get }) => ({
    total: get(todosState).length,
    completed: get(todosState).filter((todo) => todo.completed).length,
    notCompleted: get(todosState).filter((todo) => !todo.completed).length,
  }),
});

Enter fullscreen mode Exit fullscreen mode

Now create a new folder called components in the src directory and add TodoAdd.tsx and TodoList.tsx:

TodoAdd

import { useState } from "react";
import { useRecoilState, useRecoilValue } from "recoil";
import { infoValue, todosState } from "../store";
import { v4 } from "uuid";

const AddTodo = () => {
  const [title, setTitle] = useState("");
  const [todos, setTodos] = useRecoilState(todosState);
  const info = useRecoilValue(infoValue);

  return (
    <>
      <div className="alert alert-primary">
        <div className="d-inline col-4">
          Total items: &nbsp;
          <span className="badge badge-info">{info.total}</span>
        </div>
        <div className="d-inline col-4">
          Finished items: &nbsp;
          <span className="badge badge-info">{info.completed}</span>
        </div>
        <div className="d-inline col-4">
          Unfinished items: &nbsp;
          <span className="badge badge-info">{info.notCompleted}</span>
        </div>
      </div>
      <div className="form-group">
        <input
          className="form-control"
          type="text"
          value={title}
          placeholder="Todo title..."
          onChange={(e) => setTitle(e.target.value)}
        />
      </div>
      <div className="form-group">
        <button
          className="btn btn-primary"
          onClick={(_) => {
            setTodos([...todos, { id: v4(), title: title, completed: false }]);
            setTitle("");
          }}
        >
          Add Todo
        </button>
      </div>
    </>
  );
};

export default AddTodo;
Enter fullscreen mode Exit fullscreen mode

TodoList

import { useRecoilState } from "recoil";
import { todosState } from "../store";

const TodoList = () => {
  const [todos, setTodos] = useRecoilState(todosState);

  const toggleTodo = (id: string) =>
    setTodos(
      todos.map((todo) => {
        if (todo.id === id) {
          return {
            ...todo,
            completed: !todo.completed,
          };
        }
        return todo;
      })
    );
  const removeTodo = (id: string) =>
    setTodos(todos.filter((todo) => todo.id !== id));

  return (
    <>
      <div className="row">
        <table className="table table-hover">
          <thead className="thead-light">
            <tr>
              <th>Title</th>
              <th>Completed?</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {todos.map((todo) => (
              <tr key={todo.id}>
                <td>{todo.title}</td>
                <td>{todo.completed ? "" : ""}</td>
                <td>
                  <button
                    className="btn btn-sm btn-info"
                    onClick={(_) => toggleTodo(todo.id!)}
                  >
                    Toggle
                  </button>
                  <button
                    className="btn btn-sm btn-danger"
                    onClick={(_) => removeTodo(todo.id!)}
                  >
                    Remove
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </>
  );
};

export default TodoList;
Enter fullscreen mode Exit fullscreen mode

That's it, now you can run the project and manage your todo items:

Alt Text

Here's the source for the project.

Top comments (2)

Collapse
 
jaygcoder2020 profile image
Jay G

Thanks for the tutorial on this mate. Just commenting on the atom constructor. I've recently learned that you can cast the function call so you don't need to use the as operator. Something like this:

const todosState = atom<Todo[]>({
  key: "your-key",
  default: []
})
Enter fullscreen mode Exit fullscreen mode

Other than that, this is great content. Much appreciated!

Collapse
 
sirwanafifi profile image
Sirwan Afifi

Thank you for pointing it out. :) I am glad to know you enjoyed the content.