By Zain Ali
"Master advanced React with TypeScript by learning essential tools and best practices."
October 2, 2024
React with TypeScript is a powerful combination for building scalable and type-safe applications. This guide covers advanced concepts, essential tools, recommended packages, best practices, and tips to help you write clean, reusable, and efficient code. Whether you're managing state, organizing folders, or creating a clear structure, these strategies will elevate your skills.
To keep your code organized and maintainable, start with a well-structured folder setup:
src/
|-- assets/ // Static files (images, fonts, etc.)
|-- components/ // Reusable UI components
|-- hooks/ // Custom React hooks
|-- layouts/ // Page layouts
|-- pages/ // Page components
|-- services/ // API calls or external service integrations
|-- store/ // State management (e.g., Redux, Zustand)
|-- types/ // TypeScript types and interfaces
|-- utils/ // Utility functions
|-- App.tsx // Main App component
|-- index.tsx // Entry pointMyComponent.tsx), camelCase for hooks (useCustomHook.ts), and snake_case for utility functions (format_date.ts).src/ to keep everything modular and easy to locate.For an advanced React + TypeScript project, consider these essential packages:
State Management:
Styling:
Form Handling:
Routing:
Utility Libraries:
Use TypeScript's strong typing to make your code safe and predictable. Define types and interfaces to ensure consistency across components.
// types/User.ts
export interface User {
id: number;
name: string;
email: string;
}// components/UserCard.tsx
import { User } from '../types/User';
interface UserCardProps {
user: User;
}
const UserCard: React.FC<UserCardProps> = ({ user }) => (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);Avoid redundant code by creating reusable components. Use props and composition to pass down data and keep components modular.
Reusable Button Component:
// components/ui/Button.tsx
import React from 'react';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
label: string;
}
const Button: React.FC<ButtonProps> = ({ label, ...props }) => (
<button {...props}>{label}</button>
);
export default Button;Tip: Keep your components "dumb" by making them as stateless as possible and managing complex logic elsewhere, such as in hooks or parent components.
Custom hooks allow you to encapsulate and reuse complex logic across components.
// hooks/useFetchUser.ts
import { useState, useEffect } from 'react';
import { User } from '../types/User';
import axios from 'axios';
const useFetchUser = (id: number) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
axios.get<User>(`/api/users/${id}`).then((response) => setUser(response.data));
}, [id]);
return user;
};
export default useFetchUser;Redux Toolkit offers a simplified setup for Redux. Here’s a quick example:
// store/userSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { User } from '../types/User';
interface UserState {
currentUser: User | null;
}
const initialState: UserState = { currentUser: null };
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser(state, action: PayloadAction<User>) {
state.currentUser = action.payload;
},
},
});
export const { setUser } = userSlice.actions;
export default userSlice.reducer;React Query is ideal for caching server-side state and handling asynchronous data.
// components/UserProfile.tsx
import { useQuery } from 'react-query';
import axios from 'axios';
import { User } from '../types/User';
const fetchUser = async (id: number) => {
const { data } = await axios.get<User>(`/api/users/${id}`);
return data;
};
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const { data: user } = useQuery(['user', userId], () => fetchUser(userId));
if (!user) return <div>Loading...</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
};UserCard.tsx).User.ts in the types/ folder).useFetchUser.ts in the hooks/ folder).components/
|-- ui/
| |-- Button.tsx
| |-- Modal.tsx
|-- cards/
| |-- UserCard.tsx
|-- profile/
| |-- UserProfile.tsxLazy Loading Components: Use React’s React.lazy() to load components on demand, reducing initial load time.
const UserProfile = React.lazy(() => import('./UserProfile'));Memoization: Use React.memo and useCallback to avoid unnecessary re-renders.
import React, { memo } from 'react';
const ExpensiveComponent = memo(({ value }: { value: number }) => {
console.log('Rendered');
return <div>{value}</div>;
});Dependency Array: Always pass dependency arrays in useEffect and useCallback hooks to control re-renders precisely.
Use Type Aliases and Enums: Define custom types and enums to make code more readable and type-safe.
type Status = 'loading' | 'success' | 'error';
enum Roles {
ADMIN = 'ADMIN',
USER = 'USER',
}Avoid "Prop Drilling": For deeply nested components, consider using Context API or a state management solution instead of passing props down multiple levels.
DRY Principle: “Don’t Repeat Yourself” — consolidate reusable logic into utility functions or custom hooks.
By applying these advanced practices in React and TypeScript, you’ll create applications that are highly maintainable, performant, and scalable. A strong foundation with tools like Redux Toolkit, React Query, CSS-in-JS, and a solid project structure can make a significant impact on your development process. Remember, every project is unique, so adapt these guidelines to fit your needs and continue refining your practices as you grow.
Happy coding!