Todo Apps are helping users to become focused, organized, and calm.
Let’s build a simple React Todo App. It is easy and does not take much time, but it teaches you some of the most important concepts. It teaches you the principle of CRUD (Create, Read, Update, Delete) which every developer should understand. Click on the “Try it Yourself” button to see how it works.
In this article, besides React - a library that makes creating interactive UIs (User Interfaces) painless, we will use Typescript - a strongly typed programming language that builds on JavaScript, and MobX - a library that allows us to manage the application state outside the UIs.
So, let’s begin!
Part I: Initial Setup 🛠
We will need Node ≥ 14 installed in our system. You can check the Node version in your terminal by writing node --version
If you still don’t have Node on your system, you can easily download one with brew install node
using Homebrew. Or follow the official documentation on how to download and install Node.
Let’s initialize the project, then move inside the react-ts-mobx-todo-list folder, and start it. To achieve all of this, execute the following commands one by one:
# initialize the project
npx create-react-app react-ts-mobx-todo-list --template typescript
# move inside the project
cd react-ts-mobx-todo-list
# start the project
npm start
📝 npx on the first line is not a typo — it’s a package runner tool that comes with npm 5.2+
If you open http://localhost:3000/ you should see running React application.
You can use Ctrl + c (on Windows) or Control + c (on macOS) to stop running the React app in your command line.
Amazing! The project setup was successful.
Optional:
Maybe you notice already, but create-react-app command created a few files we won’t be using at all for our project. You can remove by executing the following command in CLI (Command-line interfaces), or deleting the files from GUI (Graphical user interface). If you use CLI, make sure you are inside the project folder.
# move into the src directory of your project
cd src
# delete a few files
rm -- App.test.tsx App.css logo.svg setupTests.ts
If you run the project now, you will see errors. To fix them, remove the imports in App.tsx for the file that we already deleted.
import logo from './logo.svg';
import './App.css';
and also the line where the removed logo is used.
<img src={logo} className="App-logo" alt="logo" />
If you are still facing errors, continue to the next part, since we will change completely App.tsx file.
Part II: Building the store for managing the state 📦
After successfully initializing our project, let’s move on to creating the store and the logic behind handling the data created inside the application. Inside the src folder, create a new file: storeTodo.ts
We will need to have a list of todos, and functions that will help us to add, remove and complete a todo. We want to be able to clear the list, and also to get the number of items in the list.
We should add mobx. If you are not familiar with it, check their documentation to get a brief idea of how can it help us. Also, we should add mobx-react for combining React with MobX. And uuid to create a random UUID for each todo item. Make sure you are inside the root of the project folder, and execute the following command.
npm install --save mobx mobx-react uuid @types/uuid
📝 Running the above command will install mobx, mobx-react, and uuid libraries in our application and add them to the list of dependencies inside the package.json file.
Now let’s update the storeTodos.ts file:
// storeTodos.ts
import { makeAutoObservable } from 'mobx';
import {v4 as uuidv4} from 'uuid';
export interface Todo {
id: string;
text: string;
completed: boolean;
}
const addTodoHandler = (todos: Todo[], text: string): Todo[] => [
...todos,
{
id: uuidv4(),
text,
completed: false
}
]
const removeTodoHandler = (todos: Todo[], id: string): Todo[] => todos.filter((todo) => todo.id !== id);
class StoreTodos {
todos: Todo[] = [];
constructor() {
makeAutoObservable(this);
}
addTodo(newTodo: string) {
this.todos = addTodoHandler(this.todos, newTodo);
}
removeTodo(id: string) {
this.todos = removeTodoHandler(this.todos, id);
}
toggleTodo(todo: Todo) {
todo.completed = !todo.completed;
}
clear() {
this.todos = [];
}
get size() {
return this.todos.length;
}
}
const storeTodos = new StoreTodos();
export default storeTodos;
Let’s go over the code and quickly describe it.
After the imports, we create the interface Todo, where we set all the properties we need for our to-do item. The addTodoHandler and removeTodoHandler are helper functions.
The actual store is created inside class StoreTodos. You can notice that inside constructor we use makeAutoObservable which is like makeObservable on steroids, as it infers all the properties by default.
Inference rules:
All own properties become observable - todos
All setters become action - addTodo(), removeTodo(), toggleTodo(), and clear()
All getters become computed - size()
In the last two lines, we construct an instance of the StoreTodos class using the keyword new. This calls into the constructor we defined earlier, creating a single object of the StoreTodos, and running the constructor to initialize it. Then we export the object.
Part III: Building the UIs 💅
The next step is to see the design of the application and decide how we will split the whole application into smaller components.
To be able to easily include popular icons in our React project we should add one more dependency - React Icons. Make sure you are inside the root of the project folder, and execute the following command.
npm install react-icons --save
Next, let’s create three React components inside the src folder: TodoAdd.tsx (1), TodoList.tsx (2), and TodoItem.tsx (3). The extension is .tsx because we are using TypeScript, otherwise (if we use JavaScript) it would be .jsx
Add the following snippets to their matching files.
// TodoItem.tsx
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'react';
import { AiOutlineDelete } from 'react-icons/ai';
import storeTodos, { Todo } from './storeTodos';
type TodoItemProps = {
todo: Todo;
};
const TodoItem: FunctionComponent<TodoItemProps> = ({ todo }) => {
return (
<li className='task' key={todo.id}>
<label htmlFor={todo.id}>
<input
onClick={() => storeTodos.toggleTodo(todo)}
type='checkbox'
id={todo.id}
/>
<p className={`${todo.completed ? 'checked' : ''}`}>{todo.text}</p>
</label>
<AiOutlineDelete
className='delete-icon'
onClick={() => storeTodos.removeTodo(todo.id)}
/>
</li>
);
};
export default observer(TodoItem);
// TodoList.tsx
import { observer } from 'mobx-react-lite';
import storeTodos, { Todo } from './storeTodos';
import TodoItem from './TodoItem';
const TodoList = () => {
return (
<>
<div className='controls'>
<div>TODOs</div>
<button
onClick={() => storeTodos.clear()}
className={`clear-btn ${storeTodos.size > 0 ? 'active' : ''}`}
>
Clear All
</button>
</div>
<ul className='task-box'>
{storeTodos.todos.map((todo: Todo) => (
<TodoItem todo={todo} />
))}
</ul>
</>
);
};
export default observer(TodoList);
// TodoAdd.tsx
import { observer } from 'mobx-react-lite';
import React, { useState } from 'react';
import storeTodos from './storeTodos';
import { IoIosCreate } from 'react-icons/io';
const TodoAdd = () => {
const inputChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
setNewTodo(event.target.value);
};
const keyPressHandler = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.code === 'Enter') {
storeTodos.addTodo(newTodo);
setNewTodo('');
}
};
const [newTodo, setNewTodo] = useState('');
return (
<div className='task-input'>
<IoIosCreate className='create-icon' />
<input
value={newTodo}
onChange={inputChangeHandler}
onKeyPress={keyPressHandler}
type='text'
placeholder='Add a New Task + Enter'
/>
</div>
);
};
export default observer(TodoAdd);
📝 We use VS Code editor, and we got “JSX element implicitly has type ‘any’ " error. To solve this issue, just reload VSCode. Inside the editor run Ctrl + p
(Windows) or Command + p
(macOS) and type > Developer: Reload Window
.
All three components have something in common: they import storeTodos and at the last line they are wrapped with Higher-Order Component called observer.
The observer HoC automatically subscribes React components to any observables that are used during rendering. As a result, components will automatically re-render when relevant observables change. It also makes sure that components don’t re-render when there are no relevant changes. So, observables that are accessible by the component, but not actually read, won’t ever cause a re-render. In practice, this makes MobX applications very well optimized out of the box, and they typically don’t need any additional code to prevent excessive rendering. Read on MobX and React integration for more details.
Now since we created all the needed components, let’s update our main App.tsx file and add some styling.
// App.tsx
import TodoAdd from './TodoAdd';
import TodoList from './TodoList';
function App() {
return (
<div className='wrapper'>
<TodoAdd />
<TodoList />
</div>
);
}
export default App;
We’re not going to write per-component stylesheets, so all the CSS will be added inside index.css file.
/* index.css */
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Roboto', sans-serif;
}
body {
width: 100%;
height: 100vh;
overflow: hidden;
background: linear-gradient(135deg, #f5af19, #f12711);
}
::selection {
color: #fff;
background: #f12711;
}
.wrapper {
max-width: 405px;
background: #fff;
margin: 137px auto;
border-radius: 7px;
padding: 28px 0 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.task-input {
position: relative;
height: 52px;
padding: 0 25px;
}
.task-input .create-icon {
position: absolute;
top: 50%;
color: #999;
font-size: 25px;
transform: translate(17px, -50%);
}
.task-input input {
height: 100%;
width: 100%;
outline: none;
font-size: 18px;
border-radius: 5px;
padding: 0 20px 0 53px;
border: 1px solid #999;
}
.task-input input:focus,
.task-input input.active {
padding-left: 52px;
border: 2px solid #f12711;
}
.task-input input::placeholder {
color: #bfbfbf;
}
.controls {
padding: 18px 25px;
border-bottom: 1px solid #ccc;
display: flex;
align-items: center;
justify-content: space-between;
}
li {
display: flex;
align-items: center;
justify-content: space-between;
}
.clear-btn {
border: none;
opacity: 0.6;
outline: none;
color: #fff;
cursor: pointer;
font-size: 13px;
padding: 7px 13px;
border-radius: 4px;
letter-spacing: 0.3px;
pointer-events: none;
transition: transform 0.25s ease;
background: linear-gradient(135deg, #f5af19 0%, #f12711 100%);
}
.clear-btn.active {
opacity: 0.9;
pointer-events: auto;
}
.clear-btn:active {
transform: scale(0.93);
}
.task-box {
margin-top: 20px;
margin-right: 5px;
padding: 0 20px 10px 25px;
}
.task-box.overflow {
overflow-y: auto;
max-height: 300px;
}
.task-box::-webkit-scrollbar {
width: 5px;
}
.task-box::-webkit-scrollbar-track {
background: #f12711;
border-radius: 25px;
}
.task-box::-webkit-scrollbar-thumb {
background: #e6e6e6;
border-radius: 25px;
}
.task-box .task {
list-style: none;
font-size: 17px;
margin-bottom: 18px;
padding-bottom: 16px;
align-items: flex-start;
border-bottom: 1px solid #ccc;
}
.task-box .task:last-child {
margin-bottom: 0;
border-bottom: 0;
padding-bottom: 0;
}
.task-box .task label {
display: flex;
align-items: flex-start;
cursor: pointer;
}
.task label input {
margin-top: 4px;
accent-color: #f12711;
}
.task label p {
user-select: none;
margin-left: 12px;
word-wrap: break-word;
}
.task label p.checked {
text-decoration: line-through;
}
.task .delete-icon {
cursor: pointer;
}
@media (max-width: 400px) {
body {
padding: 0 10px;
}
.wrapper {
padding: 20px 0;
}
.task-input {
padding: 0 20px;
}
.controls {
padding: 18px 20px;
}
.task-box {
margin-top: 20px;
margin-right: 5px;
padding: 0 15px 10px 20px;
}
.task label input {
margin-top: 4px;
}
}
If your app is already running, check the final result at http://localhost:3000/. If it’s not running, you can start it with the following command npm start
, executed in the application’s root folder. Inside the application, you should be able to add and remove todos.
If everything was right, we can say that our application works flawlessly! 🎉