You are viewing a preview of this lesson. Sign in to start learning
Back to React

Testing & TypeScript

Write robust tests and add type safety to applications

Testing and TypeScript in React

Master testing and TypeScript integration in React with free flashcards and spaced repetition practice. This lesson covers unit testing with Jest and React Testing Library, TypeScript configuration for React projects, type-safe component development, and end-to-end testing strategiesβ€”essential skills for building robust, maintainable React applications.

Welcome to Testing & TypeScript πŸ’»πŸ§ͺ

Welcome to one of the most crucial aspects of professional React development! Testing and TypeScript are not just "nice-to-have" additionsβ€”they're industry standards that separate hobby projects from production-ready applications. Testing ensures your components behave correctly and continue working as your codebase evolves, while TypeScript catches bugs before runtime and provides excellent developer experience through autocomplete and inline documentation.

In modern React teams, you'll rarely see a codebase without these tools. Companies like Airbnb, Slack, and Microsoft rely heavily on TypeScript and comprehensive testing to maintain their React applications. By the end of this lesson, you'll understand how to write type-safe React components, test them effectively, and integrate both practices into your development workflow.

Core Concepts: TypeScript in React πŸ“˜

What is TypeScript?

TypeScript is a superset of JavaScript that adds static type checking. Think of it as JavaScript with guardrailsβ€”it catches errors at compile time rather than runtime, making your code more predictable and maintainable.

Key benefits for React:

  • Type safety: Catch prop type mismatches before they reach production
  • IntelliSense: Get autocomplete suggestions for props, state, and methods
  • Refactoring confidence: Rename components or props knowing the compiler will catch all references
  • Self-documenting code: Types serve as inline documentation

Setting Up TypeScript in React

For new projects, create a TypeScript React app:

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

For existing projects:

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

Then rename your .js files to .tsx (for files with JSX) or .ts (for pure TypeScript).

TypeScript Configuration

The tsconfig.json file controls TypeScript behavior:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

πŸ’‘ Tip: Start with "strict": true to enable all strict type-checking options. It's harder initially but prevents bad habits.

Typing React Components

Function Components with Props

import React from 'react';

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean; // Optional prop
  variant?: 'primary' | 'secondary'; // Union type
}

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

export default Button;

πŸ’‘ Note: React.FC is optional and somewhat controversial. Many developers prefer explicit typing:

const Button = ({ label, onClick, disabled = false }: ButtonProps) => {
  // implementation
};

Class Components

import React, { Component } from 'react';

interface CounterProps {
  initialCount: number;
}

interface CounterState {
  count: number;
}

class Counter extends Component<CounterProps, CounterState> {
  constructor(props: CounterProps) {
    super(props);
    this.state = {
      count: props.initialCount
    };
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

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

Hooks with TypeScript

useState

// TypeScript infers the type
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string

// Explicit typing for complex types
interface User {
  id: number;
  name: string;
  email: string;
}

const [user, setUser] = useState<User | null>(null);

useEffect

import { useEffect } from 'react';

useEffect(() => {
  const timer = setTimeout(() => {
    console.log('Delayed action');
  }, 1000);

  // Cleanup function is properly typed
  return () => clearTimeout(timer);
}, []);

useRef

import { useRef } from 'react';

function TextInput() {
  // Type the ref with HTMLInputElement
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    // TypeScript knows inputRef.current might be null
    inputRef.current?.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </div>
  );
}

Custom Hooks

import { useState, useEffect } from 'react';

interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

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

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then((data: T) => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, [url]);

  return { data, loading, error };
}

// Usage
interface Post {
  id: number;
  title: string;
  body: string;
}

const { data, loading, error } = useFetch<Post[]>('/api/posts');

Core Concepts: Testing React Components πŸ§ͺ

Testing Philosophy

Test what users see and do, not implementation details. Your tests should resemble how users interact with your application. This approach, championed by React Testing Library, leads to more maintainable tests.

🧠 Testing Pyramid Mnemonic: "U.I. END"

           β•±β•²         END-TO-END (few)
          β•±  β•²        User flows
         ╱────╲       
        β•±      β•²      INTEGRATION (some)
       β•±        β•²     Component interaction
      ╱──────────╲    
     β•±            β•²   UNIT (many)
    ╱──────────────╲  Individual functions

Unit tests are the foundation, Integration tests verify components work together, END tests validate complete user journeys.

Setting Up Testing Environment

Jest comes pre-configured with Create React App. For React Testing Library:

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

Basic Test Structure

Tests follow the AAA pattern: Arrange, Act, Assert.

import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import Button from './Button';

describe('Button Component', () => {
  it('renders with correct label', () => {
    // Arrange
    render(<Button label="Click me" onClick={() => {}} />);
    
    // Act (implicit - just rendering)
    
    // Assert
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick handler when clicked', () => {
    // Arrange
    const handleClick = jest.fn();
    render(<Button label="Click me" onClick={handleClick} />);
    
    // Act
    fireEvent.click(screen.getByText('Click me'));
    
    // Assert
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    // Arrange & Act
    render(<Button label="Click me" onClick={() => {}} disabled={true} />);
    
    // Assert
    expect(screen.getByText('Click me')).toBeDisabled();
  });
});

Querying Elements

React Testing Library provides several query methods:

Query TypeWhen to UseReturnsThrows Error?
getBy...Element should existElementYes
queryBy...Element might not existElement or nullNo
findBy...Async element (will appear)Promise<Element>Yes
getAllBy...Multiple elements should existElement[]Yes

Query selectors in priority order:

  1. getByRole: Accessible to assistive technologies
  2. getByLabelText: For form fields
  3. getByPlaceholderText: If no label
  4. getByText: Non-interactive elements
  5. getByTestId: Last resort
// Good: Using accessible queries
screen.getByRole('button', { name: /submit/i });
screen.getByLabelText('Email address');

// Avoid: Using test IDs unless necessary
screen.getByTestId('submit-button');

Testing Async Behavior

import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';

test('loads and displays user data', async () => {
  // Mock API call
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({ name: 'John Doe', email: 'john@example.com' })
    })
  ) as jest.Mock;

  render(<UserProfile userId={1} />);

  // Initially shows loading
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // Wait for data to load
  await waitFor(() => {
    expect(screen.getByText('John Doe')).toBeInTheDocument();
  });

  // Or use findBy (combines getBy + waitFor)
  expect(await screen.findByText('john@example.com')).toBeInTheDocument();
});

Testing Hooks

Use @testing-library/react-hooks for testing custom hooks:

import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from './useCounter';

test('useCounter increments count', () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Mocking and Spying

Mock modules:

// Mock entire module
jest.mock('./api', () => ({
  fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: 'Test User' }))
}));

Mock functions:

const mockCallback = jest.fn();
mockCallback.mockReturnValue(42);
mockCallback.mockResolvedValue('async result');

// Check calls
expect(mockCallback).toHaveBeenCalledWith('expected', 'args');
expect(mockCallback).toHaveBeenCalledTimes(2);

Snapshot Testing

Snapshot tests capture component output and compare it on subsequent runs:

import { render } from '@testing-library/react';
import Card from './Card';

test('Card matches snapshot', () => {
  const { container } = render(
    <Card title="Test" description="Description" />
  );
  expect(container.firstChild).toMatchSnapshot();
});

⚠️ Warning: Use snapshots sparingly. They're brittle and don't validate functionalityβ€”only that output hasn't changed.

Examples with Explanations 🎯

Example 1: Type-Safe Form Component

Let's build a registration form with TypeScript and test it:

// RegistrationForm.tsx
import React, { useState, FormEvent } from 'react';

interface FormData {
  username: string;
  email: string;
  password: string;
}

interface RegistrationFormProps {
  onSubmit: (data: FormData) => void;
}

const RegistrationForm: React.FC<RegistrationFormProps> = ({ onSubmit }) => {
  const [formData, setFormData] = useState<FormData>({
    username: '',
    email: '',
    password: ''
  });

  const [errors, setErrors] = useState<Partial<FormData>>({});

  const validate = (): boolean => {
    const newErrors: Partial<FormData> = {};
    
    if (formData.username.length < 3) {
      newErrors.username = 'Username must be at least 3 characters';
    }
    
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
      newErrors.email = 'Invalid email address';
    }
    
    if (formData.password.length < 8) {
      newErrors.password = 'Password must be at least 8 characters';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    if (validate()) {
      onSubmit(formData);
    }
  };

  const handleChange = (field: keyof FormData) => (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormData({ ...formData, [field]: e.target.value });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="username">Username</label>
        <input
          id="username"
          type="text"
          value={formData.username}
          onChange={handleChange('username')}
        />
        {errors.username && <span role="alert">{errors.username}</span>}
      </div>

      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          value={formData.email}
          onChange={handleChange('email')}
        />
        {errors.email && <span role="alert">{errors.email}</span>}
      </div>

      <div>
        <label htmlFor="password">Password</label>
        <input
          id="password"
          type="password"
          value={formData.password}
          onChange={handleChange('password')}
        />
        {errors.password && <span role="alert">{errors.password}</span>}
      </div>

      <button type="submit">Register</button>
    </form>
  );
};

export default RegistrationForm;

Test file:

// RegistrationForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import RegistrationForm from './RegistrationForm';

describe('RegistrationForm', () => {
  it('shows validation errors for invalid input', async () => {
    const handleSubmit = jest.fn();
    render(<RegistrationForm onSubmit={handleSubmit} />);

    // Try to submit empty form
    fireEvent.click(screen.getByRole('button', { name: /register/i }));

    // Check for error messages
    expect(await screen.findByText(/username must be at least 3 characters/i)).toBeInTheDocument();
    expect(screen.getByText(/invalid email address/i)).toBeInTheDocument();
    expect(screen.getByText(/password must be at least 8 characters/i)).toBeInTheDocument();
    
    // onSubmit should not be called
    expect(handleSubmit).not.toHaveBeenCalled();
  });

  it('submits form with valid data', async () => {
    const handleSubmit = jest.fn();
    render(<RegistrationForm onSubmit={handleSubmit} />);

    // Fill in form
    await userEvent.type(screen.getByLabelText(/username/i), 'johndoe');
    await userEvent.type(screen.getByLabelText(/email/i), 'john@example.com');
    await userEvent.type(screen.getByLabelText(/password/i), 'securePassword123');

    // Submit
    fireEvent.click(screen.getByRole('button', { name: /register/i }));

    // Verify submission
    await waitFor(() => {
      expect(handleSubmit).toHaveBeenCalledWith({
        username: 'johndoe',
        email: 'john@example.com',
        password: 'securePassword123'
      });
    });
  });
});

Why this works:

  • TypeScript ensures formData structure matches FormData interface
  • Type-safe event handlers: handleChange returns a properly-typed event handler
  • Tests verify both validation and successful submission
  • Using userEvent simulates real user typing (better than fireEvent)

Example 2: Testing Component with API Call

// UserList.tsx
import React, { useState, useEffect } from 'react';

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

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

  useEffect(() => {
    fetch('/api/users')
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      })
      .then((data: User[]) => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error}</div>;

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

export default UserList;

Test file:

// UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';

const mockUsers = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];

describe('UserList', () => {
  beforeEach(() => {
    global.fetch = jest.fn();
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

  it('displays loading state initially', () => {
    (global.fetch as jest.Mock).mockImplementation(() => new Promise(() => {}));
    
    render(<UserList />);
    
    expect(screen.getByText(/loading users/i)).toBeInTheDocument();
  });

  it('displays users after successful fetch', async () => {
    (global.fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => mockUsers
    });

    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText('Alice (alice@example.com)')).toBeInTheDocument();
      expect(screen.getByText('Bob (bob@example.com)')).toBeInTheDocument();
    });
  });

  it('displays error message on fetch failure', async () => {
    (global.fetch as jest.Mock).mockResolvedValueOnce({
      ok: false
    });

    render(<UserList />);

    expect(await screen.findByText(/error: failed to fetch/i)).toBeInTheDocument();
  });
});

Key testing patterns:

  • Mock fetch globally before each test
  • Test all states: loading, success, error
  • Use waitFor to handle async state updates
  • Clean up mocks with afterEach

Example 3: Testing Context and Provider

// ThemeContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';

type Theme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  toggleTheme: () => void;
}

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

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

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

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

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

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

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

  return (
    <button 
      onClick={toggleTheme}
      style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}
    >
      Current theme: {theme}
    </button>
  );
};

export default ThemedButton;

Test file:

// ThemedButton.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import ThemedButton from './ThemedButton';

const renderWithTheme = (component: React.ReactElement) => {
  return render(
    <ThemeProvider>
      {component}
    </ThemeProvider>
  );
};

describe('ThemedButton', () => {
  it('starts with light theme', () => {
    renderWithTheme(<ThemedButton />);
    
    expect(screen.getByText(/current theme: light/i)).toBeInTheDocument();
  });

  it('toggles theme when clicked', () => {
    renderWithTheme(<ThemedButton />);
    
    const button = screen.getByRole('button');
    
    // Initially light
    expect(button).toHaveTextContent('light');
    
    // Click to toggle
    fireEvent.click(button);
    expect(button).toHaveTextContent('dark');
    
    // Click again
    fireEvent.click(button);
    expect(button).toHaveTextContent('light');
  });

  it('throws error when used outside provider', () => {
    // Suppress console.error for this test
    const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
    
    expect(() => render(<ThemedButton />)).toThrow('useTheme must be used within ThemeProvider');
    
    spy.mockRestore();
  });
});

Testing context tips:

  • Create a custom render function that wraps components in providers
  • Test context behavior through components that consume it
  • Verify error handling when context is used incorrectly

Example 4: Integration Testing

// TodoApp.tsx (integration of multiple components)
import React, { useState } from 'react';
import TodoInput from './TodoInput';
import TodoList from './TodoList';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

const TodoApp: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);

  const addTodo = (text: string) => {
    const newTodo: Todo = {
      id: Date.now(),
      text,
      completed: false
    };
    setTodos([...todos, newTodo]);
  };

  const toggleTodo = (id: number) => {
    setTodos(todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const deleteTodo = (id: number) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      <h1>Todo App</h1>
      <TodoInput onAdd={addTodo} />
      <TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
    </div>
  );
};

export default TodoApp;

Integration test:

// TodoApp.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TodoApp from './TodoApp';

describe('TodoApp Integration', () => {
  it('completes full todo workflow', async () => {
    render(<TodoApp />);

    // Add first todo
    const input = screen.getByPlaceholderText(/add a todo/i);
    await userEvent.type(input, 'Buy groceries');
    fireEvent.click(screen.getByRole('button', { name: /add/i }));

    // Verify it appears
    expect(screen.getByText('Buy groceries')).toBeInTheDocument();

    // Add second todo
    await userEvent.type(input, 'Walk the dog');
    fireEvent.click(screen.getByRole('button', { name: /add/i }));

    // Both should be visible
    expect(screen.getByText('Buy groceries')).toBeInTheDocument();
    expect(screen.getByText('Walk the dog')).toBeInTheDocument();

    // Toggle first todo
    const checkboxes = screen.getAllByRole('checkbox');
    fireEvent.click(checkboxes[0]);

    // Verify it's marked complete
    expect(checkboxes[0]).toBeChecked();

    // Delete second todo
    const deleteButtons = screen.getAllByRole('button', { name: /delete/i });
    fireEvent.click(deleteButtons[1]);

    // Verify only first todo remains
    expect(screen.getByText('Buy groceries')).toBeInTheDocument();
    expect(screen.queryByText('Walk the dog')).not.toBeInTheDocument();
  });
});

Common Mistakes ⚠️

TypeScript Mistakes

1. Using any type excessively

❌ Wrong:

const handleData = (data: any) => {
  console.log(data.name); // No type safety
};

βœ… Right:

interface UserData {
  name: string;
  age: number;
}

const handleData = (data: UserData) => {
  console.log(data.name); // Type-safe
};

2. Not typing event handlers

❌ Wrong:

const handleChange = (e) => { // Implicit any
  setValue(e.target.value);
};

βœ… Right:

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

3. Forgetting optional chaining with refs

❌ Wrong:

const inputRef = useRef<HTMLInputElement>(null);
inputRef.current.focus(); // Runtime error if null

βœ… Right:

const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus(); // Safe

Testing Mistakes

4. Testing implementation details

❌ Wrong:

it('sets state correctly', () => {
  const { result } = renderHook(() => useState(0));
  // Testing internal state management
});

βœ… Right:

it('displays updated count when button clicked', () => {
  render(<Counter />);
  fireEvent.click(screen.getByRole('button', { name: /increment/i }));
  expect(screen.getByText('Count: 1')).toBeInTheDocument();
});

5. Not waiting for async updates

❌ Wrong:

it('displays user data', () => {
  render(<UserProfile userId={1} />);
  expect(screen.getByText('John Doe')).toBeInTheDocument(); // Fails: data not loaded yet
});

βœ… Right:

it('displays user data', async () => {
  render(<UserProfile userId={1} />);
  expect(await screen.findByText('John Doe')).toBeInTheDocument();
});

6. Using wrong query methods

❌ Wrong:

expect(screen.queryByText('Error')).toBeInTheDocument(); // queryBy doesn't throw, use getBy

βœ… Right:

expect(screen.getByText('Error')).toBeInTheDocument();
// Or for non-existence:
expect(screen.queryByText('Error')).not.toBeInTheDocument();

7. Not cleaning up side effects

❌ Wrong:

beforeEach(() => {
  global.fetch = jest.fn();
});
// No cleanup - mocks leak between tests

βœ… Right:

beforeEach(() => {
  global.fetch = jest.fn();
});

afterEach(() => {
  jest.restoreAllMocks();
});

8. Over-relying on snapshots

❌ Wrong:

it('renders correctly', () => {
  const { container } = render(<ComplexComponent />);
  expect(container).toMatchSnapshot(); // Brittle, doesn't verify behavior
});

βœ… Right:

it('displays user greeting', () => {
  render(<ComplexComponent user={{ name: 'Alice' }} />);
  expect(screen.getByText('Hello, Alice')).toBeInTheDocument();
});

πŸ€” Did you know? TypeScript was created by Microsoft in 2012, and React officially adopted TypeScript types in 2016. Today, over 60% of React projects use TypeScript!

Key Takeaways 🎯

βœ… TypeScript provides type safety and better DX - catches bugs at compile time, enables autocomplete

βœ… Type your props, state, and events - use interfaces for component props, type hooks explicitly when needed

βœ… Test user behavior, not implementation - focus on what users see and do, not internal state or methods

βœ… Use React Testing Library queries wisely - prefer accessible queries like getByRole and getByLabelText

βœ… Handle async properly in tests - use waitFor, findBy, or mock promises correctly

βœ… Mock external dependencies - isolate components by mocking API calls, context, or third-party libraries

βœ… Follow the testing pyramid - many unit tests, some integration tests, few end-to-end tests

βœ… Start strict, stay strict - enable "strict": true in TypeScript from the beginning

πŸ“‹ Quick Reference Card

ConceptKey Points
TypeScript Setupnpx create-react-app my-app --template typescript
Typing Propsinterface MyProps { name: string; }
useState with TSuseState<Type | null>(null)
useRef with TSuseRef<HTMLElement>(null)
Event TypesReact.ChangeEvent<HTMLInputElement>
Testing Library@testing-library/react
Query PrioritygetByRole β†’ getByLabelText β†’ getByText
Async Testingawait screen.findByText() or waitFor()
Mock Functionsjest.fn(), mockReturnValue()
AAA PatternArrange β†’ Act β†’ Assert

πŸ“š Further Study

Congratulations! You now have the foundational knowledge to write type-safe React components and test them thoroughly. Practice by converting an existing project to TypeScript and adding comprehensive tests. Remember: TypeScript and testing are investments that pay off exponentially as your codebase grows! πŸš€