TypeScript Tutorial: Build a To-Do List

Judy@webdecoded
7 min readMay 7, 2022

--

What is a better way to get introduced to a new programming language than building a quick todo list?

For a video tutorial, please visit

https://www.youtube.com/watch?v=BUh12mwkH_8&t=476s

Without further due, let’s get started with setting up the project. For this tutorial, I used Create React App with TypeScript and ran the command in terminal:
npx create-react-app todo-app — template typescript

It will create a project folder with the name ‘todo-app’. And we can run it locally by command:

npm start

And open http://localhost:3000 in the browser to see our app.

Let’s remove the extra code in App.tsx file and add our header:

// App.tsximport React, { useState } from 'react';function App() {
return (
<div className="todo-app">
<header>
<h1>
Todo App
</h1>
</header>
</div>
);
};
export default App;

Let’s think about what components we would need: we need a form for users to enter their todo, and a list of currently existing items. Let’s create a components folder in our project and add our components there, starting with the form:

import React, { useState } from 'react';export const TodoForm = () => {
const [newTodo, setNewTodo] = useState<string>("");
return (
<form className="todo-form">
<input type="text" value={newTodo} className="todo-input" placeholder="Add a todo" />
<button type="submit" className="todo-button">
Add Todo
</button>
</form>
)
};

This form includes input — where users will type in the item they want to add and a button that will create that item. To store user's input, I also added a hook newTodo that will store the value as they are typing. As you can see, since we are using TS, we also have to declare types for new variables, hooks, functions and etc. Here since the value of the input would be text, I declared our hook as string. Now, we are able to type but the value doesn’t get updated that’s because we don’t have handleChange method, so let’s add that:

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setNewTodo(e.target.value);
}

Since it accepts an event, we have to describe what kind of event it is and we can import ChangeEvent from React at the top:

import React, { ChangeEvent } from 'react';

Let’s also add submit method:

const handleSubmit = (e: FormEvent<HTMLButtonElement>) => {
e.preventDefault();
addTodo(newTodo); --> we will pass this to the Functional component as props
setNewTodo("");
}

A few things happening here: we are defining event as `FormEvent` since submit happens as a part of the form then adding `preventDefault` so that the page doesn’t get reloaded and we don’t lose our state. Thirdly `addTodo` method will be coming from the parent component as props, because the parent will be holding the list of todo items, so it needs to know what is the new item that we want to add. And last, we update our local state of newTodo to be set to empty "" because the user already added the new item no reason that we keep the input box filled with the previous value.

Now here’s the interesting part, once we add arguments to a Functional Component, we need to type them so the component knows what types to expect. For this, I usually create an Interface , there are not many differences between types and interfaces in TS but if you want to understand what they are, you can check out my YouTube video about it.

So before the component, we can do:

interface TodoFormProps {
addTodo: (newTodo: string) => void;
}

And update the component declaration to be:

export const TodoForm: React.FC<TodoFormProps> = ({ addTodo }) => {
// rest of the code

What this does is that it will declare that props passed named addTodo is a function that returns nothing(we have void as a return value), and that it takes in one argument: newTodo that is of type string. This is sufficient information for the component for now, but we have to also create `addTodo` in the parent component before we pass it as a props and there as well we would have to type it. To make typing easier we can make use of type.d.ts file were we can define types that are used by multiple components so we don’t have to repeat the same typings multiple times. Note that d.ts extension makes sure that types defined there is imported everywhere throughout the project and we don’t have to use import statements at the top. So let’s create that file if you don’t already have it in the root of the project and add AddTodo type:

type AddTodo = (newTodo: string) => void;

Now we will modify our TodoForm component to use this type and it will look like this:

// /components/TodoForm.tsx
import React, { useState, ChangeEvent, FormEvent } from 'react';
interface TodoFormProps {
addTodo: AddTodo;
}
export const TodoForm: React.FC<TodoFormProps> = ({ addTodo }) => {
const [newTodo, setNewTodo] = useState<string>("");
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setNewTodo(e.target.value);
}
const handleSubmit = (e: FormEvent<HTMLButtonElement>) => {
e.preventDefault();
addTodo(newTodo);
setNewTodo("");
}
return (
<form className="todo-form">
<input type="text" value={newTodo} className="todo-input" placeholder="Add a todo" onChange={handleChange} />
<button type="submit" className="todo-button" onClick={handleSubmit}>
Add Todo
</button>
</form>
)
};

And to see how the form works we will add it to our App.tsx file, we would have to pass addTodo function and create a list of todo items as state:

import React, { useState } from 'react';
import './App.css';
import { TodoForm } from './components/TodoForm';
function App() {
const [todos, setTodos] = useState<Array<Todo>>([]);
const addTodo: AddTodo = newTodo => {
if (newTodo !== "") {
setTodos([...todos, { text: newTodo, complete: false }]);
}
};
return (
<div className="todo-app">
<header>
<h1>
Todo App
</h1>
</header>
<TodoForm addTodo={addTodo}/>
</div>
);
};
export default App;

and I’ll create a new type Todo that will store 2 values: the name of the todo, and the state(whether or not it’s been completed):

// types.d.tstype Todo = {
text: string;
complete: boolean;
}

Now, we can move on to the list of todos, let’s start with its smallest(last child) component which will be the list item itself:

// /components/TodoListItem.tsx
import React from "react";
interface TodoListItemProps {
todo: Todo;
toggleComplete: ToggleComplete;
}
export const TodoListItem: React.FC<TodoListItemProps> = ({ todo, toggleComplete }) => {
return (
<li>
<label className={todo.complete? "todo-row completed" : "todo-row"}>
<input
type="checkbox"
onChange={() => toggleComplete(todo)}
checked={todo.complete}
/>
{todo.text}
</label>
</li>
)
}

List items would be checkboxes, that will have the name of the Todo as text and a method toggleComplete passed to it, as well as the Todo item itself.

I used ToggleComplete as a type and will declare that in types file:

type ToggleComplete = (selectedTodo: Todo) => void;

It will take one argument, since we need to know on which list item the action was taken on but don’t need to return anything.
Moving on to its parent, we need to create a list where we can store all the items:

// /components/TodoList.tsx import React from "react";
import { TodoListItem } from './TodoListItem';
interface TodoListProps {
todos: Array<Todo>;
toggleComplete: ToggleComplete;
}
export const TodoList: React.FC<TodoListProps> = ({ todos, toggleComplete }) => {
return (
<ul>
{todos.map(todo => (
<TodoListItem
key={todo.text}
todo={todo}
toggleComplete={toggleComplete}
/>
))}
</ul>
);
};

Here we are simply taking the list of the type Todo and function we need to use once the user interacts with the todo item and mapping the items and passing functions to them.

Now that the list is ready, we can include it in our App.tsx and add function there that we need to pass to our list component, final result of App.tsx would look like this:

import React, { useState } from 'react';
import './App.css';
import { TodoForm } from './components/TodoForm';
import { TodoList } from './components/TodoList';
function App() {
const [todos, setTodos] = useState<Array<Todo>>([]);
const toggleComplete: ToggleComplete = selectedTodo => {
const updatedTodos = todos.map(todo => {
if (todo === selectedTodo) {
return { ...todo, complete: !todo.complete };
}
return todo;
});
setTodos(updatedTodos);
};
const addTodo: AddTodo = newTodo => {
if (newTodo !== "") {
setTodos([...todos, { text: newTodo, complete: false }]);
}
};
return (
<div className="todo-app">
<header>
<h1>
Todo App
</h1>
</header>
<TodoForm addTodo={addTodo}/>
<TodoList todos={todos} toggleComplete={toggleComplete} />
</div>
);
};
export default App;

And these are the styling updates I made to App.css :

.todo-app {
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 500px;
min-height: 500px;
background: white;
text-align: center;
margin: 128px auto;
border-radius: 5px;
}
h1 {
margin: 32px 0;
font-size: 24px;
}
.completed {
text-decoration: line-through;
opacity: 0.4;
}
.todo-form {
margin-bottom: 32px;
}
.todo-button {
padding: 16px;
border: none;
border-radius: 0 20px 20px 0;
cursor: pointer;
outline: none;
background: linear-gradient(
90deg,
rgba(105, 20, 204, 1) 0%,
rgba(44, 114, 251, 1) 100%
);
color: #fff;
text-transform: capitalize;
}
.todo-input {
padding: 15px 32px 15px 16px;
border-radius: 20px 0 0 20px;
border: 1px solid #dfe1e5;
outline: none;
width: 320px;
background: transparent;
}
.todo-input.edit {
border: 2px solid #149fff;
}
.todo-button.edit {
background: linear-gradient(
90deg,
rgba(20, 159, 255, 1) 0%,
rgba(17, 122, 255, 1) 100%
);
padding: 16px 22px;
}
.todo-container {
display: flex;
flex-direction: row;
position: relative;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.todo-row {
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
margin: 4px auto;
padding: 16px;
border-radius: 20px;
width: 80%;
}
.todo-row input {
margin-right: 10px;
}

Hope you enjoyed this quick tutorial:) I have another one coming up where we can edit and delete items, so make sure to subscribe not to miss it!

Photo by Thomas Bormans on Unsplash

references:
npx create-react-app my-app — template typescript

--

--

Judy@webdecoded
Judy@webdecoded

Written by Judy@webdecoded

Software Engineer | YouTuber | Web3 Enthusiast

No responses yet