React Native useEffect Hook: Complete Guide with Examples

React Native useState
React Native useState

When building applications with React Native, you often need to perform tasks such as fetching data from an API, listening for events, setting timers, or updating information when a screen becomes active.

React provides a powerful Hook called useEffect to handle these operations.

If you are new to React Native or already working with React Hooks, understanding React Native useEffect is important for writing clean and efficient applications.

In this tutorial, you will learn how to use the useEffect Hook with practical examples, including API calls, dependency arrays, cleanup functions, and common mistakes.

What Is useEffect in React Native?

useEffect is a React Hook that lets you synchronize a component with external systems.

For example, you can use it to:

  • Fetch data from an API when a screen loads.
  • Set up timers and intervals.
  • Subscribe to events.
  • Listen for changes in external data.
  • Clean up subscriptions and timers when a component is removed.

The useEffect Hook works in React Native just as it does in React for the web. It is part of React, not a React Native-specific API.

Basic Syntax of useEffect

The basic syntax looks like this:

import React, { useEffect } from 'react';
import { View, Text } from 'react-native';

const HomeScreen = () => {

  useEffect(() => {
    // Your side-effect code goes here
  }, []);

  return (
    <View>
      <Text>Welcome to React Native</Text>
    </View>
  );
};

export default HomeScreen;

The useEffect Hook accepts two main arguments:

  1. Effect function: Contains the code you want to run.
  2. Dependency array: Determines when React should run the effect again.

The dependency array is optional, but it is important to understand how it works.

1. useEffect Without a Dependency Array

If you don’t provide a dependency array, the effect runs after every committed render.

Example:

import React, { useEffect, useState } from 'react';
import { View, Text, Button } from 'react-native';

const HomeScreen = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Component rendered');
  });

  return (
    <View>
      <Text>Count: {count}</Text>

      <Button
        title="Increase Count"
        onPress={() => setCount(count + 1)}
      />
    </View>
  );
};

export default HomeScreen;

How does it work?

  • The effect runs after the initial render.
  • When you press the button, the count changes.
  • React renders the component again.
  • The effect runs again after that render.

Use this pattern carefully because it can cause unnecessary work if the component renders frequently.

2. useEffect With an Empty Dependency Array

When you provide an empty dependency array ([]), the effect runs after the component’s initial mount.

Example:

import React, { useEffect } from 'react';
import { View, Text } from 'react-native';

const HomeScreen = () => {

  useEffect(() => {
    console.log('Home screen mounted');
  }, []);

  return (
    <View>
      <Text>Home Screen</Text>
    </View>
  );
};

export default HomeScreen;

How does it work?

  • React renders the component.
  • The effect runs after the initial commit.
  • It does not run again just because the component re-renders.

This is useful for one-time setup operations.

Important: In development, React Strict Mode may run an additional setup and cleanup cycle to help identify bugs. Therefore, don’t assume an effect will always execute exactly once in every environment.

3. useEffect With Dependencies

Sometimes you want to execute an effect only when a particular value changes.

You can pass that value inside the dependency array.

Example:

import React, { useEffect, useState } from 'react';
import { View, Text, Button } from 'react-native';

const HomeScreen = () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Count updated:', count);
  }, [count]);

  return (
    <View>
      <Text>Current Count: {count}</Text>

      <Button
        title="Increase"
        onPress={() => setCount(count + 1)}
      />
    </View>
  );
};

export default HomeScreen;

Explanation:

  • The effect runs after the initial render.
  • Whenever count changes, the effect runs again.
  • If another state changes but count remains the same, this effect does not run again because of that change.

You can add multiple dependencies:

useEffect(() => {
  console.log('User information updated');
}, [userId, userName]);

The effect runs when either userId or userName changes.

4. Fetch API Data Using useEffect

One of the most common uses of useEffect in React Native is fetching data from an API.

Let’s create a simple example that fetches a list of users.

import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  FlatList,
  ActivityIndicator
} from 'react-native';

const UsersScreen = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const controller = new AbortController();

    const fetchUsers = async () => {
      try {
        const response = await fetch(
          'https://jsonplaceholder.typicode.com/users',
          { signal: controller.signal }
        );

        if (!response.ok) {
          throw new Error('Failed to fetch users');
        }

        const data = await response.json();
        setUsers(data);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    };

    fetchUsers();

    return () => {
      controller.abort();
    };
  }, []);

  if (loading) {
    return <ActivityIndicator size="large" />;
  }

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

  return (
    <View>
      <FlatList
        data={users}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <Text>{item.name}</Text>
        )}
      />
    </View>
  );
};

export default UsersScreen;

Explanation:

  1. We use useState to manage users, loading, and error states.
  2. The useEffect Hook starts the API request.
  3. The fetchUsers function retrieves data from the API.
  4. setUsers(data) updates the users state.
  5. The AbortController cancels the request if the component is cleaned up.
  6. The FlatList displays the user names.

This is a basic example of API integration using React Native useEffect.

5. Understanding the Cleanup Function

The cleanup function is an important part of useEffect.

It allows you to clean up resources created by an effect.

You should use cleanup functions when working with:

  • Timers and intervals.
  • Event listeners.
  • Subscriptions.
  • WebSocket connections.
  • Requests that support cancellation.

Example: Clear Interval Using useEffect

import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';

const TimerScreen = () => {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((previous) => previous + 1);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  return (
    <View>
      <Text>Timer: {seconds} seconds</Text>
    </View>
  );
};

export default TimerScreen;

How does it work?

  • setInterval() increases the timer every second.
  • The cleanup function calls clearInterval().
  • When the component is removed, the interval is cleared.

Without cleanup, an interval or subscription could continue running unnecessarily.

6. Listen to Events Using useEffect

You can also use useEffect to register event listeners and remove them when they are no longer needed.

For example, React Native provides AppState to track whether the application is active, inactive, or in the background.

import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  AppState
} from 'react-native';

const AppStateScreen = () => {
  const [appState, setAppState] = useState(
    AppState.currentState
  );

  useEffect(() => {
    const subscription = AppState.addEventListener(
      'change',
      nextAppState => {
        setAppState(nextAppState);
      }
    );

    return () => {
      subscription.remove();
    };
  }, []);

  return (
    <View>
      <Text>Current App State: {appState}</Text>
    </View>
  );
};

export default AppStateScreen;

Here, AppState.addEventListener() registers a listener, and the cleanup function removes it.

This is useful when you want to respond to application lifecycle changes.

7. useEffect With Dynamic API Parameters

Suppose you have a screen where users can view different products by changing a product ID.

You can use useEffect to fetch the product whenever the ID changes.

import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';

const ProductScreen = ({ productId }) => {
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const controller = new AbortController();

    const fetchProduct = async () => {
      setLoading(true);

      try {
        const response = await fetch(
          `https://fakestoreapi.com/products/${productId}`,
          { signal: controller.signal }
        );

        if (!response.ok) {
          throw new Error('Unable to fetch product');
        }

        const data = await response.json();
        setProduct(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.log(error);
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    };

    fetchProduct();

    return () => {
      controller.abort();
    };
  }, [productId]);

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

  return (
    <View>
      <Text>{product?.title}</Text>
      <Text>Price: ${product?.price}</Text>
    </View>
  );
};

export default ProductScreen;

Why do we use [productId]?

Because the API request should run again whenever the product ID changes.

The cleanup function also cancels an outdated request when a newer product ID is selected.

8. Difference Between useEffect and useState

Both useEffect and useState are React Hooks, but they have different responsibilities.

FeatureuseStateuseEffect
Main purposeManage component stateSynchronize with external systems
Updates UIYesNot directly
Stores valuesYesNo
Handles API requestsNot by itselfCan coordinate API requests
Supports cleanupNoYes

Example:

const [name, setName] = useState('John');

useEffect(() => {
  console.log('Name changed:', name);
}, [name]);

Here, useState stores the name, while useEffect reacts to changes in that name.

9. Common useEffect Mistakes in React Native

Beginners often make mistakes when working with useEffect. Here are some common issues to avoid.

Mistake 1: Forgetting the Dependency Array

useEffect(() => {
  fetchUsers();
});

Without a dependency array, the effect runs after every committed render. If the effect updates state, it may cause repeated requests or even an infinite update loop.

Better approach:

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

Use an empty dependency array only when the effect genuinely does not depend on changing reactive values.

Mistake 2: Missing Dependencies

useEffect(() => {
  console.log(userId);
}, []);

If userId changes later, the effect won’t run again.

Better approach:

useEffect(() => {
  console.log(userId);
}, [userId]);

Include the reactive values used by the effect in its dependency array.

Mistake 3: Making the Effect Callback Async

Avoid writing:

useEffect(async () => {
  const response = await fetch('https://example.com/api');
}, []);

An async function returns a Promise, but React expects an effect callback to return either nothing or a cleanup function.

Better approach:

useEffect(() => {
  const fetchData = async () => {
    const response = await fetch('https://example.com/api');
    const data = await response.json();

    console.log(data);
  };

  fetchData();
}, []);

Mistake 4: Not Cleaning Up Timers

If you create an interval or event listener, clean it up when the effect is no longer active.

useEffect(() => {
  const interval = setInterval(() => {
    console.log('Running...');
  }, 1000);

  return () => clearInterval(interval);
}, []);

Mistake 5: Using useEffect for Every Calculation

You don’t need useEffect for values that can be calculated directly from existing state or props.

Avoid:

const [price, setPrice] = useState(100);
const [tax, setTax] = useState(10);
const [total, setTotal] = useState(110);

useEffect(() => {
  setTotal(price + tax);
}, [price, tax]);

A simpler approach is:

const [price, setPrice] = useState(100);
const [tax, setTax] = useState(10);

const total = price + tax;

This avoids unnecessary state and extra renders.

10. Best Practices for Using useEffect

Follow these practices to write better React Native applications:

  1. Use dependencies correctly: Include all reactive values used inside your effect.
  2. Clean up resources: Remove event listeners, clear timers, and cancel requests when appropriate.
  3. Avoid unnecessary effects: Don’t use useEffect for simple calculations.
  4. Handle API errors: Use try/catch and show meaningful error messages.
  5. Avoid async effect callbacks: Define an async function inside the effect instead.
  6. Prevent outdated requests: Use AbortController or another appropriate cancellation strategy.
  7. Keep effects focused: Separate unrelated synchronization tasks into separate effects.
  8. Use React DevTools and linting: The React Hooks ESLint rules can help identify missing dependencies and other mistakes.

Frequently Asked Questions (FAQs)

1. What is useEffect in React Native?

useEffect is a React Hook used to synchronize a component with external systems, such as APIs, timers, subscriptions, and event listeners.

2. Does useEffect run when a screen loads?

An effect with an empty dependency array runs after the component’s initial commit. In development Strict Mode, React may perform an additional setup and cleanup cycle.

3. What is the difference between useEffect and useLayoutEffect?

useEffect runs after React commits updates and generally doesn’t block the browser or native rendering process. useLayoutEffect runs synchronously after the commit and before the screen is painted, where applicable. Use useEffect by default unless you need layout-related synchronization before painting.

4. Can we call an API inside useEffect?

Yes. You can define an asynchronous function inside useEffect and call it to fetch API data. Handle errors and clean up requests when necessary.

5. Why is the dependency array important?

The dependency array tells React which reactive values an effect depends on. React compares these values between renders and runs the effect again when a dependency changes.

6. Can we use multiple useEffect Hooks in one component?

Yes. You can use multiple useEffect Hooks to manage different side effects separately. This often makes your code easier to understand and maintain.

7. How do I stop useEffect from running repeatedly?

Check your dependency array and make sure your effect isn’t updating state in a way that continuously changes its dependencies. Include the correct dependencies rather than removing them to hide repeated execution.

Conclusion

The useEffect Hook is an essential part of React Native development. It helps you handle API requests, timers, event listeners, subscriptions, and other side effects in functional components.

In this tutorial, we covered the basic syntax, dependency arrays, API integration, cleanup functions, common mistakes, and best practices.

Start by practicing simple examples, then use useEffect in real-world projects such as authentication screens, product listings, dashboards, and API-driven applications.

Understanding React Native useEffect will help you write more organized, maintainable, and reliable applications.

Leave a Comment