Next.js & TypeScript: Building a Simple To-Do App

In the ever-evolving landscape of web development, creating dynamic and responsive user interfaces has become paramount. Developers are constantly seeking frameworks and tools that streamline the development process, enhance performance, and improve the overall user experience. Next.js, a React framework, has emerged as a frontrunner in this space, offering a powerful combination of features that simplify complex tasks. Coupled with TypeScript, a superset of JavaScript that adds static typing, Next.js provides a robust and efficient environment for building modern web applications. This tutorial will guide you through the process of building a simple to-do application using Next.js and TypeScript, providing a hands-on learning experience that will equip you with the fundamental skills to tackle more complex projects.

Why Next.js and TypeScript?

Before diving into the code, let’s explore why Next.js and TypeScript are a winning combination:

  • Next.js:
    • Server-Side Rendering (SSR) and Static Site Generation (SSG): Next.js excels at optimizing web applications for speed and SEO by rendering content on the server or at build time.
    • Routing: Next.js simplifies routing with its file-system based router, making it easy to create and manage application routes.
    • Built-in Features: Next.js offers a range of built-in features, including image optimization, API routes, and more, reducing the need for external libraries.
  • TypeScript:
    • Type Safety: TypeScript adds static typing to JavaScript, catching errors during development and improving code maintainability.
    • Code Completion and Refactoring: TypeScript enables better code completion and refactoring, boosting developer productivity.
    • Scalability: TypeScript helps manage large codebases by providing structure and preventing unexpected behavior.

Together, Next.js and TypeScript provide a powerful framework for building modern web applications with improved performance, maintainability, and developer experience. The combination allows developers to focus on building features rather than wrestling with configuration and debugging.

Setting Up the Project

Let’s get started by setting up our project. We’ll use the `create-next-app` command to bootstrap our application with TypeScript support. Open your terminal and run the following command:

npx create-next-app todo-app --typescript

This command will create a new directory named `todo-app` with the necessary files and configurations for a Next.js project with TypeScript. Navigate into the project directory:

cd todo-app

Now, let’s install any additional dependencies we’ll need. For this project, we won’t need many, but it’s good practice to set up a basic styling framework. We’ll use Tailwind CSS for this example.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

This will install Tailwind CSS and its peer dependencies, and create `tailwind.config.js` and `postcss.config.js` files. Next, configure Tailwind in your `tailwind.config.js` file:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",

    // Or if using `src` directory:
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Then, include Tailwind directives in your global CSS file (usually `styles/globals.css`):

@tailwind base;
@tailwind components;
@tailwind utilities;

Finally, let’s clear out the default content in `pages/index.tsx` and replace it with a basic structure for our to-do app:

import React from 'react';

function Home() {
  return (
    <div className="container mx-auto p-4">
      <h1 className="text-2xl font-bold mb-4">My To-Do App</h1>
      <!-- To-Do input and list will go here -->
    </div>
  );
}

export default Home;

Building the To-Do Input Component

Let’s create a component for adding new to-do items. Create a new file named `components/TodoInput.tsx`:

import React, { useState } from 'react';

interface TodoInputProps {
  onAddTodo: (text: string) => void;
}

const TodoInput: React.FC<TodoInputProps> = ({ onAddTodo }) => {
  const [inputValue, setInputValue] = useState('');

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
  };

  const handleAddTodo = () => {
    if (inputValue.trim() !== '') {
      onAddTodo(inputValue.trim());
      setInputValue('');
    }
  };

  return (
    <div className="mb-4">
      <input
        type="text"
        className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
        placeholder="Add a to-do item"
        value={inputValue}
        onChange={handleInputChange}
      />
      <button
        className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-2"
        onClick={handleAddTodo}
      >
        Add
      </button>
    </div>
  );
};

export default TodoInput;

Here’s what’s happening in this component:

  • `TodoInputProps`: We define an interface for the props the component will receive. It includes `onAddTodo`, a function that will be called when a new to-do item is added.
  • `useState`: We use the `useState` hook to manage the input field’s value.
  • `handleInputChange`: This function updates the input value as the user types.
  • `handleAddTodo`: This function is called when the user clicks the