
No matter what kind of application you are building, API calling is a mandatory part of the development process. From fetching user profiles to loading a feed of posts, your app needs to communicate with external servers.
However, a common trap many developers fall into is writing all their API calling logic directly inside their UI components. While this might work for a quick prototype, as your project grows, your files become cluttered, unorganized, and incredibly difficult to read.
In this article, we will look at the correct, scalable way to call an API in React Native. We will take a simple GET API—fetching a list of posts from https://fakestoreapiserver.reactbd.org/api/posts—and learn how to manage it beautifully by keeping our API logic within a Custom Hook.
Typically, a beginner might write a component that looks something like this:
1import React, { useState, useEffect } from 'react';
2import { View, Text, FlatList, ActivityIndicator } from 'react-native';
3const BadExampleComponent = () => {
4 const [data, setData] = useState([]);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7 useEffect(() => {
8 const fetchData = async () => {
9 try {
10 const response = await fetch('https://fakestoreapiserver.reactbd.org/api/posts');
11 const json = await response.json();
12 setData(json);
13 } catch (err) {
14 setError(err.message);
15 } finally {
16 setLoading(false);
17 }
18};
19 fetchData();
20 }, []);
21// UI rendering logic here...
22};
23While functional, this approach mixes UI rendering with data fetching. If you need to fetch posts in another part of your app, you have to duplicate this entire block of code.
Yes, you heard correctly; we will create a custom hook, similar to useState or useEffect, that handles all the heavy lifting. By abstracting the fetch logic, we adhere to the Separation of Concerns principle. Your UI component only cares about displaying the data, while the custom hook cares about getting it.
useFetch HookLet's create a new file called useFetch.js (or .ts if you are using TypeScript). This hook will take a URL as an argument and return the data, loading state, and any error that occurs.
1// hooks/useFetch.js
2import { useState, useEffect } from 'react';
3const useFetch = (url) => {
4 const [data, setData] = useState([]);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7 useEffect(() => {
8// We use an AbortController to clean up the fetch request if the component unmounts
9 const controller = new AbortController();
10
11 const fetchData = async () => {
12 setLoading(true);
13 try {
14 const response = await fetch(url, { signal: controller.signal });
15
16 if (!response.ok) {
17 throw new Error(`HTTP error! Status: ${response.status}`);
18 }
19
20 const result = await response.json();
21 setData(result);
22 setError(null);
23 } catch (err) {
24 if (err.name !== 'AbortError') {
25 setError(err.message);
26 }
27 } finally {
28 setLoading(false);
29 }
30};
31fetchData();
32 // Cleanup function
33 return () => controller.abort();
34 }, [url]);
35 return { data, loading, error };
36};
37export default useFetch;
38Why this is powerful:
AbortController to cancel the network request if the user navigates away from the screen before the API responds.Now, let's look at how incredibly clean our UI component becomes when we consume our new useFetch hook.
1// screens/PostsScreen.js
2import React from 'react';
3import { View, Text, FlatList, ActivityIndicator, StyleSheet } from 'react-native';
4import useFetch from '../hooks/useFetch';
5const PostsScreen = () => {
6 // Destructuring the values returned by our custom hook
7 const { data, loading, error } = useFetch('https://fakestoreapiserver.reactbd.org/api/posts');
8 if (loading) {
9 return (
10 <View style={styles.center}>
11 <ActivityIndicator size="large" color="#0000ff" />
12 </View>
13 );
14 }
15 if (error) {
16 return (
17 <View style={styles.center}>
18 <Text style={styles.errorText}>Error: {error}</Text>
19 </View>
20 );
21 }
22 return (
23 <View style={styles.container}>
24 <FlatList
25 data={data}
26 // Fallback to index if the API doesn't return a standard 'id' field
27 keyExtractor={(item, index) => item._id ? item._id.toString() : index.toString()}
28 renderItem={({ item }) => (
29 <View style={styles.card}>
30 {/* Adjusted slightly in case the fake store API uses different keys like 'name' or 'description' */}
31 <Text style={styles.title}>{item.title || item.name || 'Untitled Post'}</Text>
32 <Text style={styles.body}>{item.body || item.description || 'No description available.'}</Text>
33 </View>
34 )}
35 />
36 </View>
37 );
38 };
39const styles = StyleSheet.create({
40 container: { flex: 1, backgroundColor: '#f5f5f5', padding: 10 },
41 center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
42 card: { backgroundColor: '#fff', padding: 15, marginBottom: 10, borderRadius: 8 },
43 title: { fontSize: 18, fontWeight: 'bold', marginBottom: 5, textTransform: 'capitalize' },
44 body: { fontSize: 14, color: '#333' },
45 errorText: { color: 'red', fontSize: 16 }
46});
47export default PostsScreen;
48By wrapping your API calls in a custom hook, you instantly upgrade the quality of your React Native codebase. The UI components become lightweight and focused entirely on presentation, while your network logic becomes modular, reusable, and much easier to debug.
Next time you start building a feature, resist the urge to write fetch directly inside your screen components. Build a hook, keep it smart, and keep your code clean!
Discussion (0)
Please sign in to join the conversation