Home » How to use TypeScript with React
Startup

How to use TypeScript with React

TypeScript with React

In the dynamic world of web development, staying ahead of the curve is crucial. One technology stack that has gained immense popularity is the combination of React and TypeScript. React, known for its declarative and efficient UI components, when paired with TypeScript, adds a layer of static typing, making your code more robust and maintainable. In this blog post, we’ll explore the ins and outs of using TypeScript with React, helping you level up your development skills.

Understanding TypeScript

Before diving into the integration with React, let’s briefly understand TypeScript. TypeScript is a superset of JavaScript that adds static typing to the language. This means you can catch errors during development, providing a smoother development experience and reducing bugs in production. TypeScript brings the benefits of object-oriented programming to JavaScript, making your codebase more scalable and maintainable.

Key Benefits of TypeScript

  • Static Typing:

    Early Error Detection: TypeScript introduces static typing, allowing developers to catch errors during development rather than at runtime. This leads to more robust and bug-free code.
  • Enhanced Code Readability:

    Self-Documenting Code: Type annotations serve as documentation for your code. They make it easier for developers, including yourself and others, to understand the expected types of variables, function parameters, and return values.
  • Improved Development Experience:

    Intelligent Autocompletion: TypeScript’s static typing enables better autocompletion in integrated development environments (IDEs), helping developers write code more efficiently and reducing the likelihood of typos.
  • Refactoring Support:

    Safe Refactoring: Renaming variables, functions, or interfaces is safer with TypeScript, as the IDE can automatically update all references throughout the codebase, thanks to the type information.
  • Code Navigation:

    Easy Navigation: TypeScript makes it easier to navigate through large codebases. IDEs can provide quick access to the definition of a variable, function, or type, making it simpler to understand and modify code.
  • Scalability:

    Maintainable Codebase: As projects grow in size, maintaining a clear and understandable codebase becomes crucial. TypeScript’s static typing and features like interfaces and classes contribute to building scalable and maintainable applications.
  • Object-Oriented Features:

    Classes and Interfaces: TypeScript supports object-oriented programming features, such as classes and interfaces, allowing developers to organize and structure their code in a more modular and reusable manner.

TypeScript and React: A Winning Combination

Type-Safe Props and State

TypeScript allows you to define precise types for props and state, ensuring that components receive and manage data of the correct types.

// Example usage of MyComponent
<MyComponent message="Hello, TypeScript!" /> 

Event Handling

TypeScript enhances event handling by providing type information for event objects.

// Handling a button click event
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
  // Access event properties with type safety
  console.log(event.currentTarget);
};

Refactoring with Confidence

With TypeScript, refactoring becomes a safer process. The type system helps catch potential issues before runtime.

// Refactoring MyComponentProps
interface UpdatedMyComponentProps {
  updatedMessage: string;
}

const UpdatedMyComponent: React.FC<UpdatedMyComponentProps> = ({ updatedMessage }) => {
  return <div>{updatedMessage}</div>;
};

Type-Safe Hooks Usage

TypeScript brings type safety to React hooks, providing a reliable way to manage component state and lifecycle events.

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

interface MyComponentProps {
  initialCount: number;
}

const MyComponent: React.FC<MyComponentProps> = ({ initialCount }) => {
  const [count, setCount] = useState<number>(initialCount);

  useEffect(() => {
    // Side effect logic, safely using count
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default MyComponent;

TypeScript ensures that the ‘count‘ state is always of type number, preventing potential runtime errors related to incorrect state usage.

Custom React Hooks with Type Safety

TypeScript enables the creation of custom hooks with precise type definitions, enhancing the reusability of logic across components.

import { useState } from 'react';

interface UseLocalStorageProps<T> {
  key: string;
  initialValue: T;
}

function useLocalStorage<T>({ key, initialValue }: UseLocalStorageProps<T>) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error('Error retrieving data from local storage:', error);
      return initialValue;
    }
  });

  const setValue = (value: T) => {
    try {
      setStoredValue(value);
      localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error('Error storing data in local storage:', error);
    }
  };

  return [storedValue, setValue] as const;
}

// Example usage
const [username, setUsername] = useLocalStorage<string>({ key: 'username', initialValue: 'Guest' });

TypeScript ensures that the ‘useLocalStorage‘ hook is flexible yet type-safe, allowing developers to use it with different data types while maintaining code integrity.

Related: How to Use Bootstrap with React

Setting Up Your Project

The first step is to set up your React project with TypeScript. You can initiate a new project using Create React App with the TypeScript template

npx create-react-app my-react-app --template typescript

This command creates a new React project with TypeScript configuration, ensuring that TypeScript is ready to roll.

We can also add TypeScript to an Existing React Project

npm install --save typescript @types/react @types/react-dom @types/node

Create a ‘tsconfig.json‘ file in the project root to configure TypeScript.

Writing Components with TypeScript:

Now that your project is set up, let’s look at how TypeScript integrates with React components. TypeScript allows you to define the types of props, state, and function parameters, adding a layer of clarity to your code. Here’s an example of a simple React component

Functional Components

// MyComponent.tsx
import React from 'react';

interface MyComponentProps {
  message: string;
}

const MyComponent: React.FC<MyComponentProps> = ({ message }) => {
  return <div>{message}</div>;
};

export default MyComponent;

Class Components

// MyClassComponent.tsx
import React, { Component } from 'react';

interface MyClassComponentProps {
  count: number;
}

interface MyClassComponentState {
  doubledCount: number;
}

class MyClassComponent extends Component<MyClassComponentProps, MyClassComponentState> {
  constructor(props: MyClassComponentProps) {
    super(props);
    this.state = {
      doubledCount: props.count * 2,
    };
  }

  render() {
    return <div>Doubled Count: {this.state.doubledCount}</div>;
  }
}

export default MyClassComponent;

Writing React Hooks with TypeScript

React hooks are a fundamental part of modern React development, providing a way to manage state and side effects in functional components. TypeScript enhances the hook-writing experience by adding static typing to hook-related code, making it more predictable and less error-prone.

useState Hook

Let’s start with the ‘useState‘ hook, which allows functional components to manage local state. TypeScript enables us to define the type of the state, providing clarity and preventing potential type mismatches.

import React, { useState } from 'react';

interface CounterState {
  count: number;
}

const Counter: React.FC = () => {
  const [state, setState] = useState<CounterState>({ count: 0 });

  const increment = () => {
    setState((prev) => ({ count: prev.count + 1 }));
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

In this example, the ‘CounterState‘ interface defines the structure of the state, ensuring that ‘count‘ is always a number.

useReducer Hook

The ‘useReducer‘ hook is an alternative to ‘useState‘ for managing more complex state logic. TypeScript allows us to define the types for both state and action, ensuring type safety throughout the reducer logic.

import React, { useReducer } from 'react';

interface AppState {
  count: number;
}

type Action = { type: 'increment' } | { type: 'decrement' };

const counterReducer = (state: AppState, action: Action): AppState => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

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

  const increment = () => dispatch({ type: 'increment' });
  const decrement = () => dispatch({ type: 'decrement' });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  );
};

export default CounterWithReducer;

In this example, the ‘AppState‘ interface represents the shape of the state, and the ‘Action‘ type defines the possible actions that can be dispatched to the reducer.

Custom Hooks

With TypeScript, creating custom hooks becomes even more powerful, as you can precisely define the types of the hook’s parameters and return values.

import { useState, useEffect } from 'react';

interface UseFetchProps {
  url: string;
}

interface UseFetchResult {
  data: any;
  loading: boolean;
}

const useFetch = ({ url }: UseFetchProps): UseFetchResult => {
  const [data, setData] = useState<any>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('Error fetching data:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading };
};

// Usage
const MyComponent: React.FC = () => {
  const { data, loading } = useFetch({ url: 'https://api.example.com/data' });

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

  return <div>Data: {data}</div>;
};

export default MyComponent;

Here, the ‘useFetch‘ custom hook takes a ‘url‘ parameter and returns an object with ‘data‘ and ‘loading‘ properties. TypeScript ensures that the hook is used correctly, preventing potential runtime errors.

State Management with TypeScript

When it comes to managing state in React components, TypeScript offers additional benefits. By defining the types of your component’s state, you can prevent common runtime errors. Here’s an example

import React, { useState } from 'react';

interface CounterState {
  count: number;
}

const Counter: React.FC = () => {
  const [state, setState] = useState<CounterState>({ count: 0 });

  const increment = () => {
    setState((prev) => ({ count: prev.count + 1 }));
  };

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

By explicitly defining the ‘CounterState‘ type, you ensure that the ‘count‘ property is always a number, preventing potential bugs related to type mismatches.

Also Read: Top 10 Tools To Optimize Performance In React

Integrating TypeScript with React Router

If your React application involves client-side routing, integrating TypeScript with React Router is seamless. TypeScript allows you to define route parameters and props, ensuring that your routes are type-safe. Here’s a quick example

import React from 'react';
import { Route, BrowserRouter as Router, RouteComponentProps } from 'react-router-dom';

interface ProfileParams {
  username: string;
}

const Profile: React.FC<RouteComponentProps<ProfileParams>> = ({ match }) => {
  const { username } = match.params;

  return <div>Profile page of {username}</div>;
};

const App: React.FC = () => {
  return (
    <Router>
      <Route path="/profile/:username" component={Profile} />
    </Router>
  );
};

export default App;

In this example, the’ Profile‘ component receives route parameters through the ‘RouteComponentProps‘ interface, ensuring type safety when accessing the ‘username‘ parameter.

Conclusion

Incorporating TypeScript into your React projects brings numerous advantages, from improved code quality to enhanced development experience. By leveraging static typing, you can catch errors early in the development process, leading to more maintainable and scalable code. As you continue your journey with React and TypeScript, explore additional features such as generics, interfaces, and advanced type patterns to further enhance your development workflow. Happy coding!

Looking to transform these insights into impactful results? Click here to unlock a new realm of possibilities. Your journey towards innovation begins with a single Click.