Fetching API data without useEffect
And centralizing the management of API requests
Imagine you’re building a React application similar to WebMD, which provides diagnoses based on symptoms. To fetch data from the API, you’d typically use an async function inside the useEffect
hook.
However, this approach can result in infinite API calls if not managed carefully during development. Moreover, it may not always provide the data in a timely manner when the relevant component loads.
In this article, we’ll explore an alternative approach for fetching data from an API that provides a more efficient and maintainable way to ensure access to data when the application initially loads.
To begin, let’s create a new hooks
folder and inside it, a file named useFetch.js
. This file will serve as our custom hook to store the functions we’ll define in the upcoming steps.
Step 1: Create a generic function that wraps a promise
In the code above, we define the wrapPromise
function that accepts a promise and yields an object equipped with a read
method. This method operates as follows:
- Pending Status: Initially, the promise status is set to “pending”. If the
read
method is invoked during this state, it throws thesuspender
object, which is then caught by React's Suspense mechanism. - Error Status: Should an error occur, changing the status to “error”, the method throws the resultant error derived from the promise.
- Success Status: Upon successful resolution of the promise, denoted by a “success” status, the method returns the result obtained from the promise.
Step 2: Create a function that creates the necessary resources for fetching the data
- The
createResource
function returns an object with two properties:symptoms
anddiagnoses
. - Each of these properties is set to the result of calling the
wrapPromise
function on a corresponding API fetch function (fetchSymptoms
andfetchDiagnoses
, respectively). - By calling
wrapPromise
on the fetch functions, we create promise wrappers that can manage the state of the promises as they resolve or reject. This helps centralize the management of our API requests and allows us to easily handle any errors that may occur.
Step 3: Create the resource object and pass it down to child components
In this step, we first import the lazy
and Suspense
components from the React library, alongside our custom useFetch
hook. Following this, we initialize a resource
object through a function call derived from useFetch
.
- Within the return statement of the
App
component, we encapsulate theRoutes
component with aSuspense
component, assigning a fallback component to render during page loading. - The
Diagnoses
andSymptoms
components are loaded lazily, meaning they are only loaded when they are needed, and they are passed theresource.diagnoses
andresource.symptoms
properties, respectively, which contain the data fetched from the API.
To access the pertinent data directly from either component, we employ the resource.read()
method, like this:
- The
Diagnoses
component receives theresource
object as a prop, which contains the diagnoses data fetched by theuseFetch
hook. - Inside the
Diagnoses
component, theresource.read()
method is called to access the data. - The
wrapPromise
function ensures that the data is available before it is accessed, and if the data is not yet available, it throws a promise that will be caught by the parent component. - Once the data becomes accessible, it is assigned to the
allDiagnoses
variable. Then, the fetched data can be used to render the component’s UI and manage the application’s state.
Note
The resource.read()
method is exclusive to the Suspense API utilized in React for data fetching, operating in conjunction with promises. It is employed with data fetching functions generated through wrapPromise
, which returns an object encompassing the promise for data retrieval, alongside other methods for data access. It is important to note that this method is not universally applicable to all JavaScript objects or data structures.
Optimization: Caching Data and reducing API calls
If you’ve been navigating between different routes in your React app, you may have noticed that promise.read()
is making repeated API requests to fetch the same data.
There are many ways to manage application state and data. We could, for example, make use of hooks like useReducer
and useMemo
, create a React Context, or even import a library like Redux. However, there’s a simpler way to implement data caching that would allow us to integrate it with the code we’ve already written.
Step 1: Revise the createResource
Function to Incorporate Caching
Initially, the createResource
function generated an object with properties, each containing a read
method invoking wrapPromise
with each call, thereby initiating a new fetch request to the server during every invocation.
- The updated
createResource
function adds caching functionality to theread
method of each resource. This is done by creating acache
object that stores the results of the API calls for each resource. - When the
read
method is called, it checks the cache to see if the data has already been fetched before making a new API call. - This way, if the
read
method is called multiple times for the same resource, it only needs to fetch the data once and can return the cached result on subsequent calls.
Step 2: Adjust the `App` Component Accordingly
- The
resource
prop passed to theSymptoms
andDiagnoses
components has been modified to access theresource
properties of the state object. - The
useState
hook allows us to manage the state ofresource
within the component and trigger re-renders upon alterations. - Additionally, by using the
useState
hook, we can passresource
as a prop to child components, ensuring they always access the most recent version ofresource
.
useEffect vs Resource Caching
- Using
useEffect
to make multiple API calls can be useful when you need to make dynamic API requests based on the user's interaction with the UI, as the requests can be made on demand. However, this can result in redundant API calls and slower page load times if the same data is requested multiple times. - In contrast, using a resource with caching can be more efficient in terms of reducing redundant API calls and improving performance, especially when dealing with static data that doesn’t change frequently. Despite necessitating a preliminary setup for the caching mechanism and ensuring data freshness, it offers a streamlined solution for improved application performance.
Takeaways
- By using our custom
useFetch
hook to create theresource
object, we can easily manage and reuse API data without having to make multiple API requests. - The
App
component sets up the routes and passes down the API data to the child components, allowing them to render the data and manage the application’s state. - By using
lazy
andSuspense
, we can optimize the loading of our components and improve the user experience. Note that the.read()
method comes fromSuspense
and is not readily available in regular promises or other JavaScript code. - The
resource.read()
method allows us to access the API data in our child components, and ensures that the data is available before it is accessed, which improves the reliability of our application. - While there are various other strategies to accomplish this (such as
useReducer
,useMemo
, Context, Redux, and embedding async functions in atry...catch
block), it’s important to weigh the benefits and drawbacks of each approach and choose the one that is most appropriate for your specific use case.