Lesson 57 of 60 – Typing useEffect
95%

Typing useEffect

The useEffect Hook is used in React to perform side effects such as fetching data, working with timers, subscriptions, event listeners, and other operations that happen outside the normal rendering process.

Note: TypeScript usually infers the types of values used inside useEffect. However, explicitly typing API data, timer references, event handlers, and cleanup-related values can make React applications safer and easier to maintain.

1. What is useEffect?

useEffect is a React Hook used to perform side effects in a functional component.

import { useEffect } from "react";

useEffect(() => {
    console.log("Effect executed");
});

The function inside useEffect is called the effect function.

2. Importing useEffect

Import useEffect from React before using it.

import { useEffect } from "react";

It can be imported together with other React Hooks.

import { useEffect, useState } from "react";

3. Basic useEffect Syntax

useEffect(() => {

    // effect code

});

The callback function contains the code that should run as a side effect.

4. useEffect with an Empty Dependency Array

useEffect(() => {

    console.log("Component loaded");

}, []);

An empty dependency array tells React that the effect does not depend on changing values.

5. useEffect with a State Value

const [count, setCount] =
    useState<number>(0);

useEffect(() => {

    console.log(count);

}, [count]);

The effect runs when the count value changes.

6. Dependency Array

The second argument of useEffect is called the dependency array.

useEffect(() => {

    console.log("Effect");

}, [count]);

React uses the dependencies to determine when the effect should run again.

7. Multiple Dependencies

useEffect(() => {

    console.log(name);
    console.log(age);

}, [name, age]);

Multiple values can be included in the dependency array.

8. useEffect with TypeScript State

const [name, setName] =
    useState<string>("");

useEffect(() => {

    console.log(
        "Name:",
        name
    );

}, [name]);

TypeScript already knows that name is a string because of the state definition.

9. Fetching API Data

One common use of useEffect is loading data when a component starts.

useEffect(() => {

    fetch("/api/students")
        .then(response => response.json())
        .then(data => {
            console.log(data);
        });

}, []);

10. Typing API Response Data

interface Student {
    id: number;
    name: string;
    course: string;
}

const [students, setStudents] =
    useState<Student[]>([]);

The state is now prepared to store an array of typed student objects.

11. useEffect with Typed API Data

interface Student {
    id: number;
    name: string;
}

const [students, setStudents] =
    useState<Student[]>([]);

useEffect(() => {

    fetch("/api/students")
        .then(response =>
            response.json()
        )
        .then((data: Student[]) => {
            setStudents(data);
        });

}, []);

12. Loading State with useEffect

const [loading, setLoading] =
    useState<boolean>(true);

useEffect(() => {

    fetch("/api/students")
        .then(response =>
            response.json()
        )
        .then(data => {

            setStudents(data);
            setLoading(false);

        });

}, []);

A boolean state can be used to represent whether data is currently loading.

13. Error State

const [error, setError] =
    useState<string | null>(null);

A union type allows the error state to contain either a string or null.

14. Handling API Errors

useEffect(() => {

    fetch("/api/students")
        .then(response => {
            if (!response.ok) {
                throw new Error(
                    "Failed to load students"
                );
            }

            return response.json();
        })
        .then(data => {
            setStudents(data);
        })
        .catch(error => {
            setError(error.message);
        });

}, []);

15. Cleanup Function

An effect can return a cleanup function.

useEffect(() => {

    console.log("Started");

    return () => {
        console.log("Cleaned up");
    };

}, []);

Cleanup is useful for removing subscriptions, timers, and event listeners.

16. Typing setTimeout

In browser-based React applications, a timeout can be represented using ReturnType<typeof setTimeout>.

useEffect(() => {

    const timer: ReturnType<typeof setTimeout> =
        setTimeout(() => {
            console.log("Finished");
        }, 1000);

    return () => {
        clearTimeout(timer);
    };

}, []);

17. Typing setInterval

useEffect(() => {

    const interval: ReturnType<typeof setInterval> =
        setInterval(() => {
            console.log("Running...");
        }, 1000);

    return () => {
        clearInterval(interval);
    };

}, []);

The cleanup function prevents the interval from continuing after it is no longer needed.

18. Event Listener with useEffect

useEffect(() => {

    const handleResize = () => {
        console.log(window.innerWidth);
    };

    window.addEventListener(
        "resize",
        handleResize
    );

    return () => {
        window.removeEventListener(
            "resize",
            handleResize
        );
    };

}, []);

19. Why Cleanup is Important

Cleanup prevents resources from continuing to run after a component no longer needs them.

  • Removes event listeners.
  • Stops timers.
  • Stops intervals.
  • Unsubscribes from subscriptions.
  • Helps prevent unnecessary resource usage.

20. useEffect with Props

interface UserProps {
    userId: number;
}

function User({
    userId
}: UserProps) {

    useEffect(() => {

        console.log(
            "User ID:",
            userId
        );

    }, [userId]);

    return <h2>User {userId}</h2>;
}

The effect runs again when the typed userId prop changes.

21. Async Operations inside useEffect

The effect callback itself should not normally be declared directly as async. Instead, define an async function inside the effect.

useEffect(() => {

    const loadStudents = async () => {

        const response =
            await fetch("/api/students");

        const data: Student[] =
            await response.json();

        setStudents(data);
    };

    loadStudents();

}, []);

22. Async Function with Error Handling

useEffect(() => {

    const loadStudents = async () => {

        try {

            const response =
                await fetch("/api/students");

            if (!response.ok) {
                throw new Error(
                    "Unable to load students"
                );
            }

            const data: Student[] =
                await response.json();

            setStudents(data);

        } catch (error) {

            if (error instanceof Error) {
                setError(error.message);
            } else {
                setError("Unknown error");
            }

        }

    };

    loadStudents();

}, []);

23. useEffect in React Native

The same Hook can be used in React Native for tasks such as loading data or responding to state changes.

import {
    useEffect,
    useState
} from "react";

import {
    Text,
    View
} from "react-native";

function App() {

    const [message, setMessage] =
        useState<string>("Loading...");

    useEffect(() => {

        setMessage("Welcome to React Native");

    }, []);

    return (
        <View>
            <Text>
                {message}
            </Text>
        </View>
    );
}

24. useEffect with Loading and Error States

interface Student {
    id: number;
    name: string;
}

const [students, setStudents] =
    useState<Student[]>([]);

const [loading, setLoading] =
    useState<boolean>(true);

const [error, setError] =
    useState<string | null>(null);

useEffect(() => {

    const loadData = async () => {

        try {

            setLoading(true);

            const response =
                await fetch("/api/students");

            if (!response.ok) {
                throw new Error(
                    "Request failed"
                );
            }

            const data: Student[] =
                await response.json();

            setStudents(data);

        } catch (error) {

            if (error instanceof Error) {
                setError(error.message);
            }

        } finally {

            setLoading(false);

        }
    };

    loadData();

}, []);

25. useEffect with Changing ID

interface Student {
    id: number;
    name: string;
}

interface Props {
    studentId: number;
}

function StudentDetails({
    studentId
}: Props) {

    const [student, setStudent] =
        useState<Student | null>(null);

    useEffect(() => {

        const loadStudent = async () => {

            const response =
                await fetch(
                    `/api/students/${studentId}`
                );

            const data: Student =
                await response.json();

            setStudent(data);
        };

        loadStudent();

    }, [studentId]);

    return (
        <div>
            {student && (
                <h2>
                    {student.name}
                </h2>
            )}
        </div>
    );
}

When studentId changes, the effect runs again and loads the corresponding student.

26. Common useEffect Typing Mistakes

  • Using an incorrectly typed API response.
  • Forgetting to type complex state used by the effect.
  • Ignoring nullable state values.
  • Forgetting cleanup for timers or listeners.
  • Using incorrect dependency values.
  • Declaring the effect callback itself as async.
  • Using values inside an effect without considering their dependencies.

27. Dependency Array Best Practice

Values used by an effect that come from component scope should generally be considered when defining the dependency array.

useEffect(() => {

    console.log(userId);

}, [userId]);

Keeping dependencies accurate helps the effect respond to changes correctly.

28. Separating Loading, Data, and Error Types

interface Product {
    id: number;
    name: string;
    price: number;
}

const [products, setProducts] =
    useState<Product[]>([]);

const [loading, setLoading] =
    useState<boolean>(false);

const [error, setError] =
    useState<string | null>(null);

Separating these states makes API-driven components easier to understand and maintain.

29. Best Practices for Typed useEffect

  • Define interfaces for complex API responses.
  • Type state that stores API data.
  • Use string | null for nullable error states.
  • Use cleanup functions for timers and event listeners.
  • Keep the dependency array accurate.
  • Handle API errors properly.
  • Use an inner async function for asynchronous work.
  • Keep each effect focused on a clear side effect.

30. Complete Typed useEffect Example

import {
    useEffect,
    useState
} from "react";

interface Student {
    id: number;
    name: string;
    course: string;
}

function StudentList() {

    const [students, setStudents] =
        useState<Student[]>([]);

    const [loading, setLoading] =
        useState<boolean>(true);

    const [error, setError] =
        useState<string | null>(null);

    useEffect(() => {

        const loadStudents = async () => {

            try {

                setLoading(true);
                setError(null);

                const response =
                    await fetch(
                        "/api/students"
                    );

                if (!response.ok) {
                    throw new Error(
                        "Failed to load students"
                    );
                }

                const data: Student[] =
                    await response.json();

                setStudents(data);

            } catch (error) {

                if (error instanceof Error) {
                    setError(error.message);
                } else {
                    setError(
                        "Something went wrong"
                    );
                }

            } finally {

                setLoading(false);

            }
        };

        loadStudents();

    }, []);

    if (loading) {
        return <p>Loading...</p>;
    }

    if (error) {
        return <p>Error: {error}</p>;
    }

    return (
        <div>

            <h2>
                Student List
            </h2>

            {students.map(student => (

                <div key={student.id}>

                    <h3>
                        {student.name}
                    </h3>

                    <p>
                        Course: {student.course}
                    </p>

                </div>

            ))}

        </div>
    );
}

export default StudentList;

This complete example combines TypeScript interfaces, typed state, useEffect, asynchronous API loading, loading state, error state, and typed student data.

📌 Key Points

  • useEffect is used for side effects in React.
  • The dependency array controls when an effect runs again.
  • TypeScript can type API response data using interfaces.
  • Typed state can store API data safely.
  • Use string | null for nullable error states.
  • Cleanup functions are useful for timers and event listeners.
  • Use an inner async function for asynchronous operations.
  • Accurate dependencies help effects respond to changes correctly.
  • The same TypeScript patterns can be used in React Native.

🧠 Quick Quiz

Question: Which Hook is used in React to perform side effects?