Fetching API data without useEffect

And centralizing the management of API requests

Marwan Zaarab
5 min readApr 29, 2023

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

useFetch.js (part 1)

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 the suspender 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

useFetch.js (part 2)
  • The createResource function returns an object with two properties: symptoms and diagnoses.
  • Each of these properties is set to the result of calling the wrapPromise function on a corresponding API fetch function (fetchSymptoms and fetchDiagnoses, 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 the Routes component with a Suspense component, assigning a fallback component to render during page loading.
  • The Diagnoses and Symptoms components are loaded lazily, meaning they are only loaded when they are needed, and they are passed the resource.diagnoses and resource.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 the resource object as a prop, which contains the diagnoses data fetched by the useFetch hook.
  • Inside the Diagnoses component, the resource.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.


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 the read method of each resource. This is done by creating a cache 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 the Symptoms and Diagnoses components has been modified to access the resource properties of the state object.
  • The useState hook allows us to manage the state of resource within the component and trigger re-renders upon alterations.
  • Additionally, by using the useState hook, we can pass resource as a prop to child components, ensuring they always access the most recent version of resource.

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.


  • By using our custom useFetch hook to create the resource 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 and Suspense, we can optimize the loading of our components and improve the user experience. Note that the .read() method comes from Suspense 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 a try...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.