- Published on
A Comprehensive Guide to Using TypeScript with React
- Authors
- Name
- Manish Saraan
- @manish_saraan
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!