Published on

A Comprehensive Guide to Using TypeScript with React

Authors

Are you a React developer looking to level up your skills by integrating TypeScript into your projects? TypeScript is a powerful superset of JavaScript that adds static typing to your code, enabling better code quality, improved developer experience, and enhanced productivity. In this comprehensive guide, we'll explore various TypeScript concepts and demonstrate how to integrate them seamlessly into your React applications. Whether you're new to TypeScript or already familiar with it, this article will serve as a valuable reference and cheat sheet for using TypeScript with React.

A Comprehensive Guide to Using TypeScript with React

1. Getting Started with TypeScript in React

What is TypeScript?

TypeScript is a statically-typed superset of JavaScript that provides optional static type checking for your code. It helps catch errors early during development, improves code readability, and enhances code maintainability. Setting Up a React Project with TypeScript

To start using TypeScript in a React project, create a new project using create-react-app and include the --template typescript flag.

npx create-react-app my-app --template typescript
cd my-app
npm start

Now you have a React project set up with TypeScript.

Using TypeScript in React Components

TypeScript provides type annotations to specify the data types of props and state in React components. Let's look at an example:

//Hello.tsx

import React from 'react';

interface HelloProps {
  name: string;
}

const Hello: React.FC<HelloProps> = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

export default Hello;

In this example, we define the HelloProps interface with a name prop of type string. The Hello component then accepts these props and renders the greeting.

2. Type Annotations in React Components

Type Annotations for Props

Type annotations help define the data types of props and state in React components.


//Counter.tsx

import React, { useState } from 'react';

interface CounterProps {
  initialValue: number;
}

const Counter: React.FC<CounterProps> = ({ initialValue }) => {
  const [count, setCount] = useState<number>(initialValue);

  const handleIncrement = () => setCount((prevCount) => prevCount + 1);
  const handleDecrement = () => setCount((prevCount) => prevCount - 1);

  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default Counter;


In this example, the Counter component accepts a prop initialValue of type number. We use the useState hook with a type annotation to manage the state count.

Type Annotations for State

You can also specify the type of state in class components using TypeScript.

//ClassCounter.tsx

import React, { Component } from 'react';

interface ClassCounterState {
  count: number;
}

class ClassCounter extends Component<{}, ClassCounterState> {
  state: ClassCounterState = {
    count: 0,
  };

  handleIncrement = () => {
    this.setState((prevState) => ({ count: prevState.count + 1 }));
  };

  handleDecrement = () => {
    this.setState((prevState) => ({ count: prevState.count - 1 }));
  };

  render() {
    const { count } = this.state;
    return (
      <div>
        <span>Count: {count}</span>
        <button onClick={this.handleIncrement}>Increment</button>
        <button onClick={this.handleDecrement}>Decrement</button>
      </div>
    );
  }
}

export default ClassCounter;

Here, the ClassCounter component uses ClassCounterState interface to define the state shape.

3. State Management with useState and useReducer

Managing State with useState Hook

The useState hook allows you to add state to functional components.


//Counter.tsx

import React, { useState } from 'react';

const Counter: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  const handleIncrement = () => setCount((prevCount) => prevCount + 1);
  const handleDecrement = () => setCount((prevCount) => prevCount - 1);

  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default Counter;


In this example, we use useState to add state to the Counter component. The count state and the setCount function are used to manage the state.

Managing Complex State with useReducer Hook

When managing complex state transitions, useReducer hook can be more suitable.

//ReducerCounter.tsx

import React, { useReducer } from 'react';

interface CounterState {
  count: number;
}

type CounterAction = { type: 'INCREMENT' } | { type: 'DECREMENT' };

const counterReducer = (state: CounterState, action: CounterAction): CounterState => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const ReducerCounter: React.FC = () => {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  const handleIncrement = () => dispatch({ type: 'INCREMENT' });
  const handleDecrement = () => dispatch({ type: 'DECREMENT' });

  return (
    <div>
      <span>Count: {state.count}</span>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default ReducerCounter;

In this example, we define a counterReducer function that takes the current state and an action, and returns a new state based on the action type. The useReducer hook is then used to manage the state.

4. Props and Prop Types in React with TypeScript

Using Props in Functional Components

//Hello.tsx

import React from 'react';

interface HelloProps {
  name: string;
}

const Hello: React.FC<HelloProps> = ({ name }) => {
  return <div>Hello, {name}!</div>;
};

export default Hello;

Here, the Hello component accepts a prop name of type string.

Using Prop Types in Class Components

//Counter.tsx

import React, { Component } from 'react';
import PropTypes from 'prop-types';

interface CounterProps {
  initialValue: number;
}

class Counter extends Component<CounterProps> {
  static propTypes = {
    initialValue: PropTypes.number.isRequired,
  };

  state = {
    count: this.props.initialValue,
  };

  handleIncrement = () => {
    this.setState((prevState) => ({ count: prevState.count + 1 }));
  };

  handleDecrement = () => {
    this.setState((prevState) => ({ count: prevState.count - 1 }));
  };

  render() {
    const { count } = this.state;
    return (
      <div>
        <span>Count: {count}</span>
        <button onClick={this.handleIncrement}>Increment</button>
        <button onClick={this.handleDecrement}>Decrement</button>
      </div>
    );
  }
}

export default Counter;

Here, we use PropTypes from the prop-types library to specify the prop types for the Counter component.

5. Working with React Context and TypeScript

Creating a Context

// context/ThemeContext.tsx

import React, { createContext, useState, useContext } from 'react';

interface ThemeContextValue {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);

export const ThemeProvider: React.FC = ({ children }) => {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within a ThemeProvider');
  }
  return context;
};

Here, we define a ThemeContext with a ThemeContextValue interface. The ThemeProvider component provides the theme and toggleTheme values to its children using ThemeContext.Provider. The useTheme hook is used to access the context values in other components.

Using Context in Components

//ThemedButton.tsx

import React from 'react';
import { useTheme } from '../context/ThemeContext';

const ThemedButton: React.FC = () => {
  const { theme, toggleTheme } = useTheme();

  return (
    <button
      style={{ backgroundColor: theme === 'light' ? '#fff' : '#000' }}
      onClick={toggleTheme}
    >
      Toggle Theme
    </button>
  );
};

export default ThemedButton;

In this example, we use the useTheme hook to access the theme and toggleTheme values from the ThemeContext.

Handling Events in TypeScript

Handling Events in Functional Components


//EventHandling.tsx

import React, { useState } from 'react';

const EventHandling: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  const handleIncrement = () => setCount((prevCount) => prevCount + 1);
  const handleDecrement = () => setCount((prevCount) => prevCount - 1);
  const handleReset = () => setCount(0);

  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
      <button onClick={handleReset}>Reset</button>
    </div>
  );
};

export default EventHandling;

In this example, we use onClick event handlers to handle button clicks.

6. Handling Events in TypeScript

//ClassEventHandling.tsx

import React, { Component } from 'react';

interface ClassEventHandlingState {
  count: number;
}

class ClassEventHandling extends Component<{}, ClassEventHandlingState> {
  state: ClassEventHandlingState = {
    count: 0,
  };

  handleIncrement = () => {
    this.setState((prevState) => ({ count: prevState.count + 1 }));
  };

  handleDecrement = () => {
    this.setState((prevState) => ({ count: prevState.count - 1 }));
  };

  handleReset = () => {
    this.setState({ count: 0 });
  };

  render() {
    const { count } = this.state;
    return (
      <div>
        <span>Count: {count}</span>
        <button onClick={this.handleIncrement}>Increment</button>
        <button onClick={this.handleDecrement}>Decrement</button>
        <button onClick={this.handleReset}>Reset</button>
      </div>
    );
  }
}

export default ClassEventHandling;

Here, we use event handlers (onClick, onChange, etc.) to handle user interactions in class components.

7. Asynchronous Programming with TypeScript

Using Promises

//PromiseComponent.tsx

import React, { useState } from 'react';

const fetchUserData = (): Promise<string> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('John Doe');
    }, 1000);
  });
};

const PromiseComponent: React.FC = () => {
  const [userData, setUserData] = useState<string | null>(null);

  const handleClick = async () => {
    try {
      const data = await fetchUserData();
      setUserData(data);
    } catch (error) {
      console.error('Error fetching user data:', error);
    }
  };

  return (
    <div>
      {userData ? (
        <div>User Data: {userData}</div>
      ) : (
        <button onClick={handleClick}>Fetch User Data</button>
      )}
    </div>
  );
};

export default PromiseComponent;

In this example, we use a fetchUserData function that returns a promise to simulate an asynchronous operation. The handleClick function uses async/await to wait for the promise to resolve before updating the state.

Using Async Functions in Class Components

//ClassAsyncComponent.tsx

import React, { Component } from 'react';

const fetchUserData = (): Promise<string> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('John Doe');
    }, 1000);
  });
};

interface ClassAsyncComponentState {
  userData: string | null;
  loading: boolean;
}

class ClassAsyncComponent extends Component<{}, ClassAsyncComponentState> {
  state: ClassAsyncComponentState = {
    userData: null,
    loading: false,
  };

  handleClick = async () => {
    try {
      this.setState({ loading: true });
      const data = await fetchUserData();
      this.setState({ userData: data, loading: false });
    } catch (error) {
      console.error('Error fetching user data:', error);
      this.setState({ loading: false });
    }
  };

  render() {
    const { userData, loading } = this.state;
    return (
      <div>
        {userData ? (
          <div>User Data: {userData}</div>
        ) : (
          <button onClick={this.handleClick} disabled={loading}>
            {loading ? 'Loading...' : 'Fetch User Data'}
          </button>
        )}
      </div>
    );
  }
}

export default ClassAsyncComponent;

In this example, we use async/await in the handleClick function to perform the asynchronous operation in a class component.

8. Using Hooks with TypeScript

Using useState Hook

//StateHook.tsx

import React, { useState } from 'react';

const StateHook: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  const handleIncrement = () => setCount((prevCount) => prevCount + 1);
  const handleDecrement = () => setCount((prevCount) => prevCount - 1);

  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default StateHook;

Here, we use the useState hook to add state to a functional component.

Using useEffect Hook

//EffectHook.tsx

import React, { useState, useEffect } from 'react';

const EffectHook: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  const handleIncrement = () => setCount((prevCount) => prevCount + 1);
  const handleDecrement = () => setCount((prevCount) => prevCount - 1);

  return (
    <div>
      <span>Count: {count}</span>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default EffectHook;

In this example, we use the useEffect hook to perform side effects, such as updating the document title based on the count state.

9. Advanced TypeScript Techniques

Generics

//ArrayComponent.tsx

import React from 'react';

interface ArrayComponentProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

const ArrayComponent = <T extends any>({
  items,
  renderItem,
}: ArrayComponentProps<T>) => {
  return <div>{items.map(renderItem)}</div>;
};

export default ArrayComponent;

Here, we use a generic type T to make the ArrayComponent reusable for different types of items.

Conditional Types

//ConditionalComponent.tsx

import React from 'react';

type DisplayProps = {
  value: string;
};

type EditProps = {
  value: string;
  onChange: (newValue: string) => void;
};

type InputProps = DisplayProps | EditProps;

const ConditionalComponent: React.FC<InputProps> = (props) => {
  const { value, onChange } = props;

  if (onChange) {
    // Render an editable input
    return <input value={value} onChange={(e) => onChange(e.target.value)} />;
  } else {
    // Render a read-only display
    return <div>{value}</div>;
  }
};

export default ConditionalComponent;

In this example, we use conditional types to create a component that can render either an editable input or a read-only display based on the presence of the onChange prop.

10. Working with APIs and Axios

Making API Requests with Axios

//UserList.tsx

import React, { useState, useEffect } from 'react';
import axios from 'axios';

interface User {
  id: number;
  name: string;
  email: string;
}

const UserList: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    axios.get<User[]>('http://localhost:5000/users').then((response) => {
      setUsers(response.data);
      setLoading(false);
    });
  }, []);

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          <span>{user.name}</span>
          <span>{user.email}</span>
        </li>
      ))}
    </ul>
  );
};

export default UserList;

Here, we use Axios to make an API request to fetch a list of users from the server.

Wrapping up

This comprehensive guide covers various TypeScript concepts and how to integrate them effectively into React applications. From setting up a React project with TypeScript to handling events, working with context, and making API calls, you now have a solid foundation to leverage the power of TypeScript in your React projects. Use this article as a reference and cheat sheet to level up your React development with TypeScript. Happy coding!