TypeOfNaN

Your First React Typescript Project: a Todo List App

Nick Scialli
April 24, 2020

Today we’re going to learn how to write a React Typescript application. As is tradition with any new front-end language or framework, we’re going to learn how to write a to-do list app! Despite to-do list app tutorials being overdone, I like using it since you can make an apples-to-apples comparison with other frameworks in which you’ve also written the same app.

Other Resources to Accompany this Tutorial

This tutorial has an accompanying github repository! Also, I recorded a three-part YouTube tutorial series if that’s your preferred tutorial consumption method. Both can be found below:

Getting Your Environment Setup

To get started, there are a couple prerequisites. First, you’ll need to install Node if you don’t have it yet.

Make sure you have node installed by typing node -v into your command line. You should see a version display. My version is currently 10.15.2, but yours might be different.

node -v

We could use npm to manage our node packages, but I prefer yarn. Therefore, I’m going to use npm to install yarn globally: npm i -g yarn

npm i -g yarn

If this works, you should be able to see your yarn version by typing yarn -v. Again, your version may be different than mine:

yarn -v

Now we’re ready to get going!

Bootstrapping with create-react-app

To save us the setup headache and get us moving faster, we can bootstrap our app with create-react-app! I’ve used React in production a lot, and I still generally start out with create-react-app as a template to get going.

Let’s use yarn to create a react. We’ll need to make sure to specify that we want to use Typescript and that we want to name our app todo-list:

yarn create react-app todo-list --template typescript

You should see a bunch of downloading happening and finally instructions to cd into your new directory and start coding. Let’s go!

Exploring the Bootstrapped App

Make sure you’re in the new todo-list directory. Your should see the following folders and files. While we’ll be doing most of our work in the src folder, it’s important to understand what everything else does. Here’s a quick rundown:

  • node_modules - contains code for the third-party libraries used by your app.
  • public - contains the assets that’ll help build your final app, including things like index.html and your app’s favicon.
  • src - contains the source code for the app you’ll be working with most.
  • .gitignore - specifies which files to ignore for source control.
  • package.json - contains your app’s configuration, including things like dependencies and scripts.
  • README.md - starts with information about create-react-app, but in a real application you should describe the app itself.
  • tsconfig.json - contains configuration for the typescript compiler.
  • yarn.lock - contains the exact version for all project dependencies. Should be checked into version control.

Start the app

Great, that’s enough looking around. Let’s start the app by running yarn start in the command prompt.

Navigate to http://localhost:3000 and you should see our app in all its starter glory:

default create-react-app

Note: As part of the deal with create-react-app, our app will hot reload whenever we make changes! That means we can generally leave yarn start running in the console and we won’t have to restart it. Realistically, we’ll find that our app occasionally does require us to restart the server when the typescript compiler barfs or we add or remove files.

Stripping Down to “Hello World”

This is cool and all, but we want to start relatively fresh with this tutorial. Therefore, we’re going to remove a bunch of files from the src folder and modify some files.

Remove files

cd src
rm App.css App.test.tsx index.css logo.svg serviceWorker.ts setupTests.ts

The only files left should be App.tsx, index.tsx, and react-app-env.d.ts.

Edit code

Let’s first hop into index.tsx and remove references to index.css and the service worker. Your file should end up looking like this:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

Next, open up App.tsx and delete references to the logo and CSS file. Also, delete everything from the App function and instead replace it with returning a React fragment with the text “Hello World” in it.

import React from 'react';

function App() {
  return <>Hello World</>;
}

export default App;

Now check out our app!

hello world app

A Quick Mock of Our Todo List Application

One nice aspect of React is your component structure can often closely follow your design. In our todo list application example, we can pretent we’re given the following mock:

todo list app mock

Importantly, we can see our app has a TodoListItem, a TodoList, and an AddTodoForm. Ultimately, our app structure will end up mirroring this.

Creating a Todo List Item

Let’s get to work! Create a new file called TodoListItem.tsx in your src folder.

Let’s write a basic React component that has placeholder content inside a list item:

import React from 'react';

export const TodoListItem = () => {
  return <li>content</li>;
};

Cool. Now, let’s Add some props. This is where we’ll start getting typescripty! Our TodoListItem component will at least take a todo item as a prop. That todo item will have text property, which will be a string, and a complete property, which will be a boolean.

Once we’ve defined our props, we can declare our TodoListItem as a functional component (React.FC) and then pass our Props as a generic.

import React from 'react';

interface Todo {
  text: string;
  complete: boolean;
}

interface Props {
  todo: Todo;
}

export const TodoListItem: React.FC<Props> = (props) => {
  return <li>content</li>;
};

Next, let’s actually use the props that we’ve described. Let’s put a checkbox in each list item. The checkbox will be checked when todo.complete is true. A label will be populated with our todo.text.

Additionally, let’s put a strike-through our todo item if it’s complete. We can do this with the style attribute.

import React from 'react';

interface Todo {
  text: string;
  complete: boolean;
}

interface Props {
  todo: Todo;
}

export const TodoListItem: React.FC<Props> = ({ todo }) => {
  return (
    <li>
      <label
        style={{ textDecoration: todo.complete ? 'line-through' : undefined }}
      >
        <input type="checkbox" checked={todo.complete} /> {todo.text}
      </label>
    </li>
  );
};

Creating a Type Declaration File

While we could keep Todo declared in this file, it’s going to be used throughout the app. We can either export it here and import it everywhere else we need it throughout the app, or we can create a type declaration file. Let’s call this types.d.ts and put it in our src folder. What’s nice about a *.d.ts file is our compiler will recognize the types in there as global to the project and we won’t need to explicitly import or export them.

types.d.ts

interface Todo {
  text: string;
  complete: boolean;
}

Now we can remove the Todo interface declared in TodoListItem.tsx and everything should still work just fine.

Including The TodoListItem in our App

Of course we’ve only written a component so far; we still need to include it in our App. let’s do that now. Go to App.tsx and import the component.

import React from 'react';
import { TodoListItem } from './TodoListItem';

function App() {
  return (
    <>
      <TodoListItem />
    </>
  );
}

export default App;

You may notice that, if we try to run our app right now, it’ll fail to compile–we defined our TodoListItem as taking a todo prop and we’re not providing it! Let’s change that: we’ll make a Todos array.

We’ll create two items and put them within an unordered list:

import React from 'react';
import { TodoListItem } from './TodoListItem';

const todos: Todo[] = [
  {
    text: 'Walk the dog',
    complete: false,
  },
  {
    text: 'Write app',
    complete: true,
  },
];

function App() {
  return (
    <ul>
      <TodoListItem todo={todos[0]} />
      <TodoListItem todo={todos[1]} />
    </ul>
  );
}

export default App;

Now let’s check out our app in the browser:

beginning of the todo app

Toggling Todos

The next thing we want to do is actually be able to toggle the todo items. We can no longer rely on our todos array, but we’ll rather need some state to manage things. For this, we’ll use the useState React hook in our App.tsx file. We can rename our todos array to initialTodos since it really just represents the initial state.

import React, { useState } from 'react';
import { TodoListItem, Todo } from './TodoListItem';

const initialTodos: Todo[] = [
  {
    text: 'Walk the dog',
    complete: false,
  },
  {
    text: 'Write app',
    complete: true,
  },
];

function App() {
  const [todos, setTodos] = useState(initialTodos);
  return (
    <ul>
      <TodoListItem todo={todos[0]} />
      <TodoListItem todo={todos[1]} />
    </ul>
  );
}

export default App;

We’ll want to be able to toggle todos. We can do this by creating a toggleTodo function in our App.tsx file. The toggleTodo function will take a selected todo and toggle the complete prop for that todo.

Then, we can pass toggleTodo to each TodoListItem.

import React, { useState } from 'react';
import { TodoListItem } from './TodoListItem';

const initialTodos: Todo[] = [
  {
    text: 'Walk the dog',
    complete: false,
  },
  {
    text: 'Write app',
    complete: true,
  },
];

function App() {
  const [todos, setTodos] = useState(initialTodos);

  const toggleTodo = (selectedTodo: Todo) => {
    const newTodos = todos.map((todo) => {
      if (todo === selectedTodo) {
        return {
          ...todo,
          complete: !todo.complete,
        };
      }
      return todo;
    });
    setTodos(newTodos);
  };

  return (
    <ul>
      <TodoListItem todo={todos[0]} toggleTodo={toggleTodo} />
      <TodoListItem todo={todos[1]} toggleTodo={toggleTodo} />
    </ul>
  );
}

export default App;

Our linter is now mad. That’s because toggleTodo is not an expected prop for our TodoListItem. Let’s add it as an expected prop. While we’re at it, let’s declare a ToggleTodo type in our types.d.ts file:

types.d.ts

interface Todo {
  text: string;
  complete: boolean;
}

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

Now when we add toggleTodo as a prop for the TodoListItem, let’s execute it in the onClick handler for our input element.

TodoListItem.tsx

import React from 'react';

interface Props {
  todo: Todo;
  toggleTodo: ToggleTodo;
}

export const TodoListItem: React.FC<Props> = ({ todo, toggleTodo }) => {
  return (
    <li>
      <label
        style={{ textDecoration: todo.complete ? 'line-through' : undefined }}
      >
        <input
          type="checkbox"
          checked={todo.complete}
          onClick={() => {
            toggleTodo(todo);
          }}
        />{' '}
        {todo.text}
      </label>
    </li>
  );
};

Let’s open our app and start toggling our todo items. It works!

toggling todos

Creating a TodoList Component

If you recall, our app mock include a TodoList component to contain all our todos.

todo list app mock

Let’s create that component. It’s going to have to take the following props:

  • List of todos to map over
  • The toggleTodo function to pass to each todo item.

Importent to note in this component is that we’re mapping over our todos rather than listing them individually. This is obviously a good idea because we could theoretically have any number of todos. Note that, when we iterate over the todos, we pass each TodoListItem a key prop. This is needed by React’s diffing algorithm to reconcile arrays of elements.

TodoList.tsx

import React from 'react';
import { TodoListItem } from './TodoListItem';

interface Props {
  todos: Todo[];
  toggleTodo: ToggleTodo;
}

export const TodoList: React.FC<Props> = ({ todos, toggleTodo }) => {
  return (
    <ul>
      {todos.map((todo) => (
        <TodoListItem key={todo.text} todo={todo} toggleTodo={toggleTodo} />
      ))}
    </ul>
  );
};

Now, we can replace much of the code we had in our App.tsx file with our TodoList. We have to remember to pass the right props to it–although if we forget, the typescript compiler will yell at us, which is great!

App.tsx

import React, { useState } from 'react';
import { TodoList } from './TodoList';

const initialTodos: Todo[] = [
  {
    text: 'Walk the dog',
    complete: false,
  },
  {
    text: 'Write app',
    complete: true,
  },
];

function App() {
  const [todos, setTodos] = useState(initialTodos);

  const toggleTodo = (selectedTodo: Todo) => {
    const newTodos = todos.map((todo) => {
      if (todo === selectedTodo) {
        return {
          ...todo,
          complete: !todo.complete,
        };
      }
      return todo;
    });
    setTodos(newTodos);
  };

  return <TodoList todos={todos} toggleTodo={toggleTodo} />;
}

export default App;

If we open up our app in the browser, we should be able to confirm that everything’s working.

Adding Todo Items

Let’s make a new component called AddTodoForm so we can add todo items. For now, we’ll just make a form that doesn’t do anything and add it to our App.tsx file.

AddTodoForm.tsx

import React from 'react';

export const AddTodoForm: React.FC = () => {
  return (
    <form>
      <input type="text" />
      <button type="submit">Add Todo</button>
    </form>
  );
};

App.tsx

import React, { useState } from 'react';
import { TodoList } from './TodoList';
import { AddTodoForm } from './AddTodoForm';

const initialTodos: Todo[] = [
  {
    text: 'Walk the dog',
    complete: false,
  },
  {
    text: 'Write app',
    complete: true,
  },
];

function App() {
  const [todos, setTodos] = useState(initialTodos);

  const toggleTodo = (selectedTodo: Todo) => {
    const newTodos = todos.map((todo) => {
      if (todo === selectedTodo) {
        return {
          ...todo,
          complete: !todo.complete,
        };
      }
      return todo;
    });
    setTodos(newTodos);
  };

  return (
    <>
      <TodoList todos={todos} toggleTodo={toggleTodo} />
      <AddTodoForm />
    </>
  );
}

export default App;

Now we can see in our browser the form appears. When we try to add a todo and click submit, nothing really happens except the page reloads.

with form

Now, let’s make our form add stuff. First, we can create an addTodo function in our App.tsx file that will eventually be passed down to our form. We can declare the type AddTodo in our types.d.ts file.

Since each new todo will start out incomplete, we really just need to text prop to create one.

types.d.ts

interface Todo {
  text: string;
  complete: boolean;
}

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

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

App.tsx

import React, { useState } from 'react';
import { TodoList } from './TodoList';
import { AddTodoForm } from './AddTodoForm';

const initialTodos: Todo[] = [
  {
    text: 'Walk the dog',
    complete: false,
  },
  {
    text: 'Write app',
    complete: true,
  },
];

function App() {
  const [todos, setTodos] = useState(initialTodos);

  const toggleTodo: ToggleTodo = (selectedTodo: Todo) => {
    const newTodos = todos.map((todo) => {
      if (todo === selectedTodo) {
        return {
          ...todo,
          complete: !todo.complete,
        };
      }
      return todo;
    });
    setTodos(newTodos);
  };

  const addTodo: AddTodo = (text: string) => {
    const newTodo = { text, complete: false };
    setTodos([...todos, newTodo]);
  };

  return (
    <>
      <TodoList todos={todos} toggleTodo={toggleTodo} />
      <AddTodoForm addTodo={addTodo} />
    </>
  );
}

export default App;

Again, we’ll hit a familiar compilation error at this point: AddTodoFrom doesn’t expect an addTodo prop, so the compiler barfs. Good! Let’s fix that up by adding the prop to our AddTodoForm.

import React from 'react';

interface Props {
  addTodo: AddTodo;
}

export const AddTodoForm: React.FC<Props> = ({ addTodo }) => {
  return (
    <form>
      <input type="text" />
      <button type="submit">Add Todo</button>
    </form>
  );
};

Now our compiler error is gone, but our form still doesn’t do anything. To get it to work, we’ll do a number of things:

  1. Maintain internal text state using useState. This will allow us to maintain the state of the new todo item’s text.
  2. Bind text to the input value.
  3. Set text using setText in the input’s onChange handler. e.target.value contains the current value.
  4. Add an onClick handler to the submit button to submit the typed text.
  5. Make sure to cancel the default event of actually submitting the form.
  6. Add the todo using addTodo and passing it text.
  7. Clearing our form by setting text to an empty string.
import React, { useState } from 'react';

interface Props {
  addTodo: AddTodo;
}

export const AddTodoForm: React.FC<Props> = ({ addTodo }) => {
  const [text, setText] = useState('');

  return (
    <form>
      <input
        type="text"
        value={text}
        onChange={(e) => {
          setText(e.target.value);
        }}
      />
      <button
        type="submit"
        onClick={(e) => {
          e.preventDefault();
          addTodo(text);
          setText('');
        }}
      >
        Add Todo
      </button>
    </form>
  );
};

And there you have it! If you go back to the application, you should now be able to add new todo items and interact with them.

finished app

Conclusion

Thanks for following along! This should hopefully give you a nice head start on your journey to using React with Typescript to craft incredible user interfaces.

If you'd like to support this blog by buying me a coffee I'd really appreciate it!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli