DEV Community

SOVANNARO
SOVANNARO

Posted on

7 React Lessons I Wish I Knew Earlier (with TypeScript Examples)

React is a powerful library for building user interfaces, but it can be tricky to master, especially when you're just starting out. Over time, I’ve learned several lessons that have significantly improved my React development skills. In this article, I’ll share seven React lessons I wish I had known earlier, complete with TypeScript examples to help you write cleaner, more maintainable code.


1. Understand the Component Lifecycle (and Hooks)

When I first started with React, I didn’t fully grasp the component lifecycle. With the introduction of hooks in React 16.8, managing lifecycle events became more intuitive, but it’s still crucial to understand how they work.

Class Components vs. Functional Components with Hooks

In class components, lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount are used. With functional components, you can achieve the same functionality using hooks like useEffect.

Example with TypeScript:

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

const LifecycleExample: React.FC = () => {
  const [count, setCount] = useState<number>(0);

  useEffect(() => {
    console.log('Component mounted or updated');

    return () => {
      console.log('Component will unmount');
    };
  }, [count]); // Runs when `count` changes

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

export default LifecycleExample;
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Use useEffect to handle side effects and lifecycle events in functional components. Always clean up resources (e.g., event listeners) in the return function to avoid memory leaks.


2. Use TypeScript for Type Safety

TypeScript is a game-changer for React development. It helps catch errors at compile time, provides better autocompletion, and makes your code more maintainable.

Typing Props and State

Always define types for your component props and state. This ensures that your components are used correctly and reduces runtime errors.

Example with TypeScript:

interface UserProps {
  name: string;
  age: number;
  isActive: boolean;
}

const User: React.FC<UserProps> = ({ name, age, isActive }) => {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Status: {isActive ? 'Active' : 'Inactive'}</p>
    </div>
  );
};

export default User;
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Use TypeScript interfaces or types to define props and state. This makes your components more predictable and easier to debug.


3. Leverage Context API for State Management

For small to medium-sized applications, you don’t always need a state management library like Redux. React’s Context API is a lightweight alternative for sharing state across components.

Creating and Using Context

Create a context using createContext and provide it at the top level of your component tree. Use the useContext hook to consume the context in child components.

Example with TypeScript:

import React, { createContext, useContext, useState } from 'react';

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

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

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

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

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

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

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

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

export { ThemeProvider, ThemedButton };
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Use the Context API for global state management when Redux is overkill. Always provide a custom hook (e.g., useTheme) for consuming context to avoid undefined values.


4. Optimize Performance with React.memo and useMemo

React re-renders components whenever their state or props change. For complex components, this can lead to performance issues. Use React.memo and useMemo to optimize rendering.

Preventing Unnecessary Re-renders

React.memo memoizes a component, preventing re-renders if its props haven’t changed. useMemo memoizes expensive calculations.

Example with TypeScript:

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

interface ExpensiveComponentProps {
  value: number;
}

const ExpensiveComponent: React.FC<ExpensiveComponentProps> = React.memo(({ value }) => {
  console.log('ExpensiveComponent re-rendered');
  return <div>Value: {value}</div>;
});

const OptimizedComponent: React.FC = () => {
  const [count, setCount] = useState<number>(0);
  const doubleCount = useMemo(() => count * 2, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveComponent value={doubleCount} />
    </div>
  );
};

export default OptimizedComponent;
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Use React.memo for components with expensive renders and useMemo for expensive calculations. Avoid premature optimization—only optimize when you notice performance issues.


5. Use Custom Hooks for Reusable Logic

Custom hooks allow you to encapsulate and reuse logic across multiple components. This keeps your components clean and focused on rendering.

Creating a Custom Hook

A custom hook is just a function that uses other hooks. By convention, its name should start with use.

Example with TypeScript:

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError('Failed to fetch data');
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Extract reusable logic into custom hooks to keep your components clean and maintainable.


6. Write Tests for Your Components

Testing is essential for ensuring your components work as expected. Use tools like Jest and React Testing Library to write unit and integration tests.

Testing a Component

Write tests to verify that your components render correctly and handle interactions as expected.

Example with TypeScript:

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('increments counter on button click', () => {
  render(<Counter />);
  const button = screen.getByText(/Increment/i);
  const count = screen.getByText(/Count: 0/i);

  fireEvent.click(button);
  expect(count).toHaveTextContent('Count: 1');
});
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Write tests for your components to catch bugs early and ensure they behave as expected.


7. Use Functional Components and Hooks Over Class Components

Functional components with hooks are now the standard in React. They are simpler, more concise, and easier to test compared to class components.

Migrating from Class to Functional Components

If you’re still using class components, consider migrating them to functional components with hooks.

Example with TypeScript:

// Class Component
class Greeting extends React.Component<{ name: string }> {
  render() {
    return <h1>Hello, {this.props.name}!</h1>;
  }
}

// Functional Component with Hooks
const Greeting: React.FC<{ name: string }> = ({ name }) => {
  return <h1>Hello, {name}!</h1>;
};
Enter fullscreen mode Exit fullscreen mode

Key Takeaway: Prefer functional components and hooks for new code. They are more modern and easier to work with.


Conclusion

React is a versatile library, but mastering it takes time and practice. By understanding the component lifecycle, leveraging TypeScript, optimizing performance, and writing tests, you can build robust and maintainable applications. Start applying these lessons today, and you’ll see a significant improvement in your React development skills. Happy coding! 🚀

Top comments (0)