Loading...

Understanding Side Effects in React: A Guide with TypeScript

Understanding Side Effects in React: A Guide with TypeScript
4 min read
0Views
reacttypescriptjavascript

In React, side effects refer to anything that affects something outside the scope of a function component, such as updating the DOM, fetching data, or interacting with external APIs. While React encourages declarative programming, side effects are often inevitable when building interactive UIs. Properly managing these effects is crucial for maintaining predictable(testable) and maintainable applications.

What are Side Effects in React?

In React, side effects typically occur when a component interacts with external systems or modifies shared states. Examples include:

  • Fetching data from an API.
  • Updating a DOM element directly.
  • Logging information to an external service.
  • Setting up subscriptions or timers. React's useEffect hook is the primary tool for handling side effects in functional components.

Fetching Data

Fetching data is a common side effect. Here’s how you can use useEffect to fetch data in a React component:

import React, { useEffect, useState } from "react";
 
import Loading from "@/component/Loading";
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
const UserList: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
 
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/users");
        const data: User[] = await response.json();
        setUsers(data);
      } catch (error) {
        console.error("Error fetching users:", error);
      } finally {
        setLoading(false);
      }
    };
 
    fetchUsers();
  }, []);
 
  return (
    <div>
      {loading ? (
        <Loading />
      ) : (
        <ul>
          {users.map(({ id, name, email }) => (
            <li key={id}>
              {name} ({email})
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};
 
export default UserList;

In this example:

  • The useEffect hook fetches user data when the component mounts.
  • The empty dependency array ([]) ensures the effect runs only once.
  • The setUsers function modifies the state, which is a controlled side effect.

side-effects-in-react

Cleanup with Side Effects

Some side effects, like subscriptions or timers, require cleanup to prevent memory leaks. React allows you to return a cleanup function from useEffect.

import React, { useEffect, useState } from "react";
 
const Timer: React.FC = () => {
  const [count, setCount] = useState<number>(0);
 
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(prevCount => prevCount + 1);
    }, 1000);
 
    return () => clearInterval(interval);
  }, []);
 
  return <h1>Timer: {count} seconds</h1>;
};
 
export default Timer;

In this example:

  • An interval is created in the useEffect hook.
  • The cleanup function clears the interval when the component unmounts, preventing memory leaks.

Conditional Effects

Sometimes, effects depend on specific state or prop changes. This is where dependencies come into play.

import React, { useEffect, useState } from "react";
 
const SearchComponent: React.FC = () => {
  const [query, setQuery] = useState<string>("");
  const [results, setResults] = useState<string[]>([]);
 
  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
 
    const fetchResults = async () => {
      try {
        const response = await fetch(`https://api.example.com/search?q=${query}`);
        const data: string[] = await response.json();
        setResults(data);
      } catch (error) {
        console.error("Error fetching search results:", error);
      }
    };
 
    fetchResults();
  }, [query]);
 
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map((result, index) => (
          <li key={index}>{result}</li>
        ))}
      </ul>
    </div>
  );
};
 
export default SearchComponent;

In this example:

  • The useEffect hook re-runs only when the query state changes.
  • The dependency array [query] ensures unnecessary API calls are avoided.

Avoiding Unnecessary Re-Renders

React re-renders components when states or props change, which can trigger effects unnecessarily. Using tools like useCallback and useMemo can optimize performance.

import React, { useCallback, useEffect, useState } from "react";
 
const ExpensiveComponent: React.FC = () => {
  const [count, setCount] = useState<number>(0);
 
  const logCount = useCallback(() => {
    console.log("Count updated:", count);
  }, [count]);
 
  useEffect(() => {
    logCount();
  }, [logCount]);
 
  return <button onClick={() => setCount(count + 1)}>Increment Count: {count}</button>;
};
 
export default ExpensiveComponent;

useCallback memoizes the logCount function, ensuring it only updates when count changes. This prevents unnecessary re-creation of the function and optimizes the effect's behavior.

Learn more about useEffect

Share with your network: