Outline
- Intro to Data Fetching in React
- Simple Explanation of JavaScript Promises
- Approach #1: Fetch API w/ Promise Methods
- Approach #2: Axios Library w/ Promise Methods
- Approach #3: Async Functions (async / await)
- Approach #4: Creating ‘useFetch’ Custom React Hook
- Approach #5: React Query Library
- Approach #6: Redux Toolkit’s RTK Query
- Some Final Thoughts
Link to Follow Along w/ Examples
Data Fetching in React
Data fetching is a core aspect of any React application. It is very important for React developers to understand the different data fetching approaches and which is most appropriate for their use case.
This article has a corresponding public GitHub repository that you can reference to better understand. The sample React app fetches data from an external API (random.dog) and renders a random picture of a dog on the webpage. Its goal is to be very simple to allow for easy comparison between the data fetching approaches.
But first, let's understand JavaScript Promises
Simply put, a promise is a JavaScript object that will produce a value sometime in the future. This usually applies to asynchronous operations (ex. data fetching).
A promise has three states:
- Pending: where the promise is still in the works
- Fulfilled: where the promise resolves successfully and returns a value
- Rejected: where the promise fails with an error
If a promise is fulfilled or rejected it has settled. Promises have different methods for doing different things depending on the outcome. These methods will be discussed in greater detail in the next sections.
Approach #1 - Fetch API w/ Promise Methods
The Fetch API provides a global fetch()
method that enables developers a straightforward approach to fetching data. Prior to fetch()
, the conventional approach was using XMLHttpRequest()
. (This approach is not included in this article as fetch()
has replaced it with a more powerful and flexible feature set.)
The fetch()
method requires one parameter, the URL to request, and returns a promise. The second and optional parameter, options, is an array of properties. The return value of fetch()
can be either JSON or XML (either an array of objects or a single object). Without the options parameter, fetch()
will always make a GET request.
This first approach is what you will typically see in simple use cases of data fetching and often the first result when navigating API documentation. As stated before, we are fetching data from an API that returns a random image of a dog and we then render that image on screen. Prior to making the request, we wrap the code inside a useEffect
hook with an empty dependency array to run the fetch()
method only when the component initially mounts.
Full Code for Approach #1
useEffect(() => {
fetch(URL)
// syntax for handling promises
.then((res) => {
// check to see if response is okay
if (res.ok) {
// if okay, take JSON and parse to JavaScript object
return res.json();
}
throw res;
})
// .json() returns a promise as well
.then((data) => {
console.log(data);
// setting response as the data state
setData(data);
})
// if res is not okay the res is thrown here for error
.catch((err) => {
console.error(`Error: ${err}`);
// setting the error state
setError(err);
})
// regardless if promise resolves successfully or not we remove loading state
.finally(() => {
setLoading(false);
});
}, []);
Inside the useEffect
we call the fetch()
method and pass in the URL for our API endpoint. In this approach we are using the .then()
, .catch()
, .finally()
methods of a promise object (recall that fetch()
returns a promise). We use the .then()
method and pass in a callback function to check if the response is ok. If the response is ok, we take the JSON data that is returned and parse it into a JavaScript object with the .json()
method. If the response is not ok, we throw
an error.
Since the .json()
method also returns a promise we can chain another .then()
and pass a function that sets the state of the data to then be used elsewhere in the component. In our example, the external API returns an object with a url property (will be used as src
of our image).
Continuing through the chain, the next section is the .catch()
that schedules a function to be called when the promise is rejected. This also returns another promise where we can then chain the .finally()
method that will be called regardless of if the promise is settled (either resolved or rejected). The .finally()
method allows us to avoid duplicating code in both .then()
and .catch()
, making this a good place to remove the loading state in our example.
Approach #2 - Axios Library w/ Promise Methods
Axios is a popular HTTP client library that can be used for efficient data fetching. It can be easily installed via npm or other package managers into React applications. Using Axios is an alternative to the Fetch API and has some advantages if you do not mind installing an external library.
This second example will be very close to our code for the first example using the same promise methods for handling promise state and responses. Instead of using the fetch()
method, after importing the Axios library into our component we can use the axios.get()
method where we can pass in the URL to our external API endpoint. This will return a promise so we can take the same approach with the promise method chaining.
Full Code for Approach #2
useEffect(() => {
axios.get(URL)
// syntax for handling promises
.then((res) => {
console.log(res.data);
// axios converts json to object for us (shortens our code)
setData(res.data);
})
// axios takes care of error handling for us instead of checking manually
.catch((err) => {
console.error(`Error: ${err}`);
// setting the error state
setError(err);
})
// regardless if promise resolves successfully or not we remove loading state
.finally(() => {
setLoading(false);
});
}, []);
The apparent differences between the code for the Fetch API and the code for this Axios approach is that with Axios we only need a single .then()
as Axios converts the JSON to a JavaScript object for us (shortening our code). Additionally, we are no longer writing a conditional to throw errors manually because Axios throws 400 and 500 range errors for you (again shortening our code).
Approach #3 - Async Functions (async / await)
In this example we will be moving away from the promise chaining we have used in the previous two examples and instead introduce a more modern approach for writing asynchronous, promise-based code. This approach can be used with whatever fetching mechanism you choose but for this example we will be sticking with the Axios library.
This third example sets up the component in a similar way to the last example with importing the Axios library and then wrapping the code for fetching the data inside of a useEffect
with an empty dependency array. Within the useEffect
, we create an asynchronous function using the async
keyword and then within the function have three separate sections - try
, catch
, and finally
. This try/catch approach is used to handle errors in JavaScript. The code inside of the try
block is executed first and if there are any errors thrown they will be ‘caught’ in the catch
block and the code inside will be executed. Lastly, the finally
block will always be executed after the flow passes through the try/catch.
Full Code for Approach #3
useEffect(() => {
// create async function b/c cannot use async in useEffect arg cb
const fetchData = async () => {
// with async/await use the try catch block syntax for handling
try {
// using await to make async code look sync and shorten
const res = await axios.get(URL);
setData(res.data);
} catch (err) {
console.error(`Error: ${err}`);
// setting the error state
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
In this example, the try
block creates a variable called res
(short for response) that uses the async
keyword. This allows the code to look synchronous (shorter and easier on the eyes). In this example the axios.get(URL)
is being ‘awaited’ until it settles. If the promise is fulfilled, then we set the data into the state. If the promise is rejected (errors thrown), it moves into the catch
block.
Approach #4 - Creating ‘useFetch’ Custom React Hook
The fourth approach is creating our own custom React hook called useFetch
that will be able to be reusable across our app in different components and cut out the bulky fetching code from each component. This example is really just taking the fourth example (same technique of using the Axios library with async/await) and moving that code into its own custom hook.
To do this, we create a new file called useFetch.js
. We then take all of the code within the useEffect
from the last example as well as the different states we are tracking add it inside of the useFetch
function. Finally this function will return an object with each of those states to then be accessed where the useFetch
hook is called. Our useFetch
hook will also accept one parameter, the URL, to allow for more reusability with the potential to make fetch requests to different endpoints.
Full Code for Approach #4
const useFetch = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// create async function b/c cannot use asyc in useEffect arg cb
const fetchData = async () => {
// with async/await use the try catch block syntax for handling
try {
// using await to make async code look sync and shorten
const res = await axios.get(url);
setData(res.data);
} catch (err) {
console.error(`Error: ${err}`);
// setting the error state
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
return {
data,
loading,
error,
};
};
Finally, we import this new custom hook into the components where it will be used and then call it as we would any other React hook. As you can see this greatly helps with code readability and shortening our component.
One final note with this approach is that there are also external libraries that you can install rather than creating your own custom hook. One popular library is react-fetch-hook
with a very similar functionality to the hook we just built.
Approach #5 - React Query Library
One of the most modern and robust approaches for data fetching in React is using the React Query library. It has many features in addition to just simple data fetching but for this example we will learn just how to simply fetch data from the same example external API. (If you are interested in learning more check it out here)
After installing and importing, React Query provides many custom hooks that can be reused across our components in a very concise way. In this example, we import
QueryClient
and QueryClientProvider
from react-query
and then wrap our application with the provider and pass the queryClient
instance as the client
property to the wrapper. This enables us to use the library across our app.
To make this simple GET request we import and use the useQuery
hook. Unlike the previous example with our custom hook, we pass in two parameters. The first required parameter is the queryKey that is used as a key for reference for this specific query. The second required parameter is the queryFn that is the function that the query will use to request data. Rather than just passing in a simple URL like our previous custom hook example we will use this query function and then use the Fetch API and promise method syntax to make the initial fetch. (This hook has many other optional parameters.)
Full Code for Approach #5
const { isLoading, error, data } = useQuery("dogData", () => fetch(URL).then((res) => res.json()));
From here, React Query will do all of the additional work behind the scenes and in this example we can destructure isLoading
, error
, and data
from this hook call to be used in our application although there are many other values we are able to access as well.
The power and advantages of using React Query are evident in examples that are larger than our current Dog Image API example. A few additional features to mention include: caching, updating ‘out of date’ data in the background, and other performance related benefits.
Approach #6 - Redux Toolkit’s RTK Query
The final approach of this article is data fetching with Redux Toolkit’s RTK Query. It is very common for apps to be using Redux for state management. If your company or your current side project is currently using Redux, a good option is to use RTK Query for data fetching as it provides similar simplicity and benefits as React Query does.
To start using RTK Query wherever you are storing your Redux code, create a rtkQueryService.js
file that will be the setup for data fetching. After creating you then add the service to your Redux store and assuming you are already using Redux you will already have a <Provider>
component with the store that is wrapped around your application.
From here, it is very similar to using the custom hook and React Query approach where you import and then use the query hook and destructure data
, error
, and isLoading
to then be able to use in your component.
Full Code for Approach #6
const { data, error, isLoading } = useGetDogQuery();
As you can see there is a lot of setup for Redux so this might not be the best approach for our use case but RTK Query can be valuable if you are already using Redux within your React app and want a simple and modern approach for data fetching that also provides benefits like caching.
Some Final Thoughts
Props to you if you got to this point! The goal of this article was to introduce some different approaches of data fetching for those learning React. This serves as a centralized place to compare with the same use case to better understand. It was not my goal to make statements on the ‘best’ approach or dive into granular detail.
Additionally, there are other current approaches to data fetching not mentioned here and I am sure others will come about as the React ecosystem evolves. That said, I believe this article provides a strong foundation for understanding this space. I hope you found this useful!
(As this was my first post on dev.to I am curious to hear your feedback in the comments! What is your preferred way to fetch data in your React apps?)