135 Macbeath Ave, Moncton, NB E1C 6Z8, Canada
Back to Blog
August 8, 2025 John Smith 10 min read

React with TypeScript: Best Practices for Scalable Applications

React TypeScript Frontend Best Practices
React with TypeScript: Best Practices for Scalable Applications

Combining React with TypeScript has become the industry standard for building robust, maintainable, and scalable web applications. When used effectively, TypeScript can dramatically improve your development experience and code quality.

In this comprehensive guide, we'll explore the best practices for using TypeScript with React to create applications that are not only type-safe but also easier to maintain and scale as your project grows in complexity.

Why TypeScript with React?

TypeScript brings static typing to JavaScript, which provides several benefits for React development:

  • Early error detection: Catch errors during development rather than at runtime
  • Improved developer experience: Better autocompletion, navigation, and refactoring tools
  • Self-documenting code: Types serve as documentation for your components and functions
  • Easier refactoring: Confidence when making changes to large codebases
  • Better collaboration: Clear contracts between different parts of your application

"TypeScript is like a seatbelt for your codebase—it might feel restrictive at first, but it saves you from serious injuries when things go wrong."

1. Properly Typing Component Props

The foundation of TypeScript in React is properly typing your component props. Here are the best practices:

Use Interfaces for Props

Prefer interfaces over type aliases for component props as they are more extensible:

interface UserCardProps {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
  isActive: boolean;
  onStatusChange: (userId: number, isActive: boolean) => void;
  createdAt?: Date; // Optional property
}

const UserCard: React.FC<UserCardProps> = ({ 
  id, 
  name, 
  email, 
  role, 
  isActive, 
  onStatusChange,
  createdAt 
}) => {
  return (
    <div className="user-card">
      <h3>{name}</h3>
      <p>{email}</p>
      <span className={`role-badge ${role}`}>{role}</span>
      <button 
        onClick={() => onStatusChange(id, !isActive)}
        className={isActive ? 'active' : 'inactive'}
      >
        {isActive ? 'Deactivate' : 'Activate'}
      </button>
      {createdAt && (
        <p>Joined: {createdAt.toLocaleDateString()}</p>
      )}
    </div>
  );
};

Define Default Props Properly

For components with default props, use the following pattern:

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick: () => void;
  children: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({ 
  variant = 'primary',
  size = 'medium',
  disabled = false,
  onClick,
  children 
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size} ${disabled ? 'disabled' : ''}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
};

2. Typing Component State

Properly typing your component state is crucial for avoiding runtime errors:

Use Type Inference When Possible

For simple state, let TypeScript infer the type:

const [count, setCount] = useState(0); // Type inferred as number
const [name, setName] = useState(''); // Type inferred as string
const [isLoading, setIsLoading] = useState(false); // Type inferred as boolean

Explicitly Type Complex State

For more complex state objects, explicitly define the type:

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

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  error: string | null;
}

const AuthComponent: React.FC = () => {
  const [authState, setAuthState] = useState<AuthState>({
    user: null,
    isAuthenticated: false,
    isLoading: false,
    error: null
  });

  // Component logic...
};

3. Typing Events

Properly typing events ensures you access the correct properties:

const InputField: React.FC = () => {
  const [value, setValue] = useState('');

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

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Submit logic...
  };

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.stopPropagation();
    // Click logic...
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="text" 
        value={value} 
        onChange={handleChange} 
      />
      <button type="submit" onClick={handleClick}>
        Submit
      </button>
    </form>
  );
};

4. Creating Custom Hooks with TypeScript

Custom hooks benefit greatly from TypeScript's type system:

interface UseApiResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

function useApi<T>(url: string): UseApiResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const fetchData = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  }, [url]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

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

const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
  const { data: user, loading, error } = useApi<User>(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
};

5. Advanced Patterns: Generic Components

Create flexible, reusable components with generics:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyMessage?: string;
}

function List<T>({ 
  items, 
  renderItem, 
  keyExtractor, 
  emptyMessage = "No items found" 
}: ListProps<T>) {
  if (items.length === 0) {
    return <div className="empty-state">{emptyMessage}</div>;
  }

  return (
    <ul className="list">
      {items.map(item => (
        <li key={keyExtractor(item)}>
          {renderItem(item)}
        </li>
      ))}
    </ul>
  );
}

// Usage with different data types
interface Product {
  id: number;
  name: string;
  price: number;
}

interface User {
  userId: string;
  firstName: string;
  lastName: string;
}

const ProductList: React.FC<{ products: Product[] }> = ({ products }) => {
  return (
    <List
      items={products}
      keyExtractor={(product) => product.id}
      renderItem={(product) => (
        <div>
          <h3>{product.name}</h3>
          <p>${product.price.toFixed(2)}</p>
        </div>
      )}
    />
  );
};

const UserList: React.FC<{ users: User[] }> = ({ users }) => {
  return (
    <List
      items={users}
      keyExtractor={(user) => user.userId}
      renderItem={(user) => (
        <div>
          <h3>{user.firstName} {user.lastName}</h3>
        </div>
      )}
    />
  );
};

6. Organizing Types in Large Applications

For large applications, organize your types effectively:

Use a Centralized Types Directory

// types/index.ts
export * from './user';
export * from './product';
export * from './api';

Create Domain-Specific Type Files

// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: UserRole;
  createdAt: Date;
  updatedAt: Date;
}

export type UserRole = 'admin' | 'user' | 'moderator';

export interface UserCreateRequest {
  name: string;
  email: string;
  password: string;
  role?: UserRole;
}

export interface UserUpdateRequest {
  name?: string;
  email?: string;
  role?: UserRole;
}

7. Testing with TypeScript

Leverage TypeScript in your tests for better reliability:

// Component test example
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders with correct text and handles click', () => {
    const handleClick = jest.fn();
    
    render(
      <Button variant="primary" onClick={handleClick}>
        Click me
      </Button>
    );
    
    const button = screen.getByText('Click me');
    expect(button).toBeInTheDocument();
    
    fireEvent.click(button);
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('applies correct classes based on props', () => {
    const { container } = render(
      <Button variant="danger" size="large" disabled>
        Delete
      </Button>
    );
    
    const button = container.firstChild;
    expect(button).toHaveClass('btn-danger');
    expect(button).toHaveClass('btn-large');
    expect(button).toHaveClass('disabled');
  });
});

Conclusion

TypeScript and React are a powerful combination that can significantly improve your development experience and application quality. By following these best practices, you'll create applications that are more robust, maintainable, and scalable.

Remember that TypeScript is a tool to help you, not hinder you. Start with simple type annotations and gradually adopt more advanced patterns as your comfort level grows. The investment in learning TypeScript will pay dividends throughout your React development career.

At Yukon Gold Exclusive, we incorporate TypeScript throughout our curriculum to ensure our graduates are equipped with the skills needed for modern React development. Whether you're just starting or looking to level up your skills, understanding how to effectively use TypeScript with React is essential for building production-ready applications.

Stay Updated with Our Latest Insights

Subscribe to our newsletter and never miss new articles, coding tips, and industry updates from Yukon Gold Exclusive.

We respect your privacy. Unsubscribe at any time.