xBlog

BLOG

Data fetching in React

Data fetching in React
Mohamed EL AYADI

Mohamed EL AYADI

08 February, 2021 Β· 10min πŸ“–

In this post, we will talk about fetching data in SPA and in React, and we will discuss some tips to achieve effective and consistent data fetching.

Image from Federico Beccari on Unsplash

Disclaimer:

This post will not discuss or use a specific data fetching library, but will focus on the basics of data fetching using React, the do and don't and some examples.

We'll be using an imaginary function called request(url, options) . We won't be using any third party helper library to orchestrate our data fetching (like react-queryor any similar), because the goal is to reveal what they do behind the scenes and what are some of the problems they protect us from.

Plan:

  • Motivations
  • Types of data fetching
  • Data fetching fundamentals
  • What is a side effect ?
  • The data fetching state
  • Can't perform a React state update on an unmounted component
  • User input optimizations (debounce and throttle)
  • Conclusion

Motivations

Data fetching is common in all Β SPA projects, you need to fetch data after your page has been loaded, and then to give it a UI representation. There are multiple implementations, patterns and third party libraries that try to make this task the simplest possible.

Although data fetching sounds easy: make an xml http request and update the UI in your host environment, but there are common mistakes and anti-patterns that you may find in some code bases. Have you ever seen requests sent twice ? with an undefined parameter ? What about multiple requests while the user is typing, and the last one resolves first ? your search results may be wrong despite your API answered right. These are common problems I saw in React projects I maintained.

In this post, we will discuss these problems, their causes and how to deal with them.

Types of Data fetching

There are multiple forms and types of data fetching:

  • Static data: when the fetched data is used in a static context, like a dropdown or some sort of tags.
  • Paginated list of data with filters.
  • Lazy loaded resource data: let's say you need to display data inside a tab (while having multiple tabs), but only fetch if the tab is visited.
  • Data update (post, put, patch, delete...)

Obviously there are other forms of data fetching (like real time communication, data streaming, b2b... But that's another story for a future post) we will be focusing only on these types because they are the most used in enterprise applications.

Data fetching basics

When fetching data, you have to be aware of the following aspects. The more your fulfill, the more efficient your data-fetching is.

  1. The fetch status isn't a Boolean: a true/false as fetch status is misleading, you can't distinguish between the success and error (unless you add another boolean for that). For the rest of this post, we will assume that a component fetching data passes through 4 possible states, with 5 possible transitions between them: initial|loading|success|error where the possible transitions are: initial -> loading, loading -> success|error, success|error -> loading.
  2. Display a loading indicator: without an indicator, the user may think his operation haven't started. But how every one of my components may be aware of its data fetching status ? What type of loading indicator to use ? a modal or an inline spinner ? What to do when data arrives too fast and the loader flashes ? Hang-on, we will answer all of these questions in a minute, let's continue.
  3. Display a clear and understable error message (api error, network error/failure...): a fetch operation may fail for many I/O errors, plus api errors.
  4. Retrayable: In case of a retryable api error (timeout, general failure or scheduled maintenance), the user need to retry the exact previous call easily.
  5. Clean concurrency: the order at which the fetch requests are resolved is important, and may differ from the requesting order.
  6. Back-button safe.

We will go back to these aspects in a while, and we will dig deeper while giving pratical examples.

So, to sum up, data fetching is an unpredictable operation in so many ways (failures, transitions, concurrency, networking...), and that's what we call a side effect.

What is a side effect ?

Given fixed inputs, you can't predict your function's output at each call: that's an impure function, or a function with a side effect.

By analogy, calling a pure function indefinitely by the same inputs will always output the same result.

So, by now, we know that data fetching is a side effect operation, which should be managed carefully in a React application. On the other hand, React is a javascript UI runtime that runs your components (that "should" be pure functions around state and props) while providing good APIs to deal with side effect: React.useEffect(or lifecycle methods in class components). Let's first refresh our memory about useEffect before going any longer:

// the signature
function useEffect(create, deps) {}

useEffect accepts two parameters:

  1. The effect creator via the create argument: create is a function invoked whenever deps change. If your effect creator returns a function (and it should while fetching data!), it will be invoked if the deps change or the component is unmounted, and it is called in this case: the cleanup function.
  2. The optional dependencies array: an array containing some javascript values, that, if one of them change, the previous effect will be cleaned (if there is a cleanup function), and a new effect will be invoked. If this parameter is omitted, the effect will run every time the component renders.

Working with dependencies is so far the most exciting aspect for me: it allows a synchronization between multiple values to do a common job (and clean it!):

React.useEffect(
    function () {
        fetchData(filters, pagination);
    },
    [filters, pagination] // when filters or pagination change, reload data
);

We won't be using only useEffect obviously, sometimes you need to trigger fetch operations when a button is clicked for example. In this case, you will be firing an asynchronous function, that, when resolved, gives you the data (or the error).

This brings us to the fetching state: the fetch status, and the data.

The fetching state

Now, we will compose our data fetching state (and let's just not discuss at what level it should be hoisted!).

The fetch state will be composed of the fetch status and the content:

const FetchStatus = Object.freeze({
	INITIAL: "initial",
    LOADING: "loading",
    SUCCESS: "success",
    ERROR: "error",
});

const initalState = Object.freeze({
	status: FetchStatus.INITIAL,
    content: null,
});
const loadingState = {
	status: FetchStatus.LOADING,
    content: { requestData: { page: 0, size: 10 } }
};
const successState = {
	status: FetchStatus.SUCCESS,
    content: { data: {/*...*/}, requestData: { username: "Sophie" } }
};
const errorState = {
	status: FetchStatus.ERROR,
    content: { error: {/*...*/}, requestData: { bankId: 12 } }
};

It is important to keep a copy of the request data, this gives the ability to retry the request or to refresh it (and that's what we want).

Let's write the signature of one of the APIs that will help us build efficient data fetching:

function useFetchOperation({
	// will be used to controle when to start the fetch
    // for example: condition=isHovered, condition=!!debouncedUsername
	condition,
    // defines whether this fetch action can be reloadable
    reloadable,
    // a callback fired (if present) when the fetch state changes
    onStateChange,
    
    /* depending on your project, this is the part that you may tune:
    - if working with redux, you can pass the actionCreator and its arguments
    - if working with axios directly, the promise and its arguments go here
    */
    // the url
    url,
    // the request function options
    options,
    
    retryable,
    retryDelay,
    maxRetries,
    
    indicator = true,
    // the component to be mounted if indicator is true
    // ts receives the fetch state and the reload function
    // can be used to tune if to display an inline loader or a dialog one
    IndicatorComponent,
    // contextual props to the indicator component
    IndicatorComponentProps,
}) {
	/**
    * ... magic goes here
    * Not exactly magic, but to shorten this post, I'll omit this part
    * but it will be accessible in the codesandbox examples ;-)
    */
    React.useEffect(() => {
    	reload();
    }, [memoizedDependencies.current]);
    
	return {
        state,
        reload,
        error: state.status === FetchStatus.ERROR,
        loading: state.status === FetchStatus.LOADING,
        success: state.status === FetchStatus.SUCCESS,
    	indicator: indicator && <IndicatorComponent {...IndicatorProps} />,
    };
}

This API can solve a lot of headaches of almost all GET requests in a declarative way: When my url/options change, I load data. So all you need to do, is to store your data search values in a state variable, so whenever they change, a new fetch operation is done, and the previous is forgotten/aborted.

This API can be forked in so many ways that may cover all of your needs:

  • Specialized fetch triggers
return (
	<>
		<FetchTrigger url="/pokemons" options={ type: ["fire", "water"] } onSuccess={fn} onError={fn} />
        <ComponentAccessingDataSomehow />
    </>
);
  • Manually triggerred operations
const { perform } = useManualFetchOperation(/* ... */)
// later
<button onClick={perform}>Click me</button>
  • With fetch triggers variants
// will not have an indicator (ie silent requests)
function useGhostFetchOperation({ ...args }) {
	return useFetchOperation({ ...args, indicator: false });
}
// will display a dialog with spinner when loading
// and thus preventing the user from clicking in the page
function useGlobalFetchOperation({ ...args }) {
	return useFetchOperation({ ...args, IndicatorComponent: DialogIndicator });
}
// ... and so on

Let's now dig deeper into our previously discussed aspects and see how we can solve them:

  1. The data fetching status isn't a true/false variable: we know the importance of this aspect. So whenever working with a local state object or a reducer, or even a global state management solution, we should keep track of the fetch status and the request data (to replicate it if needed).
  2. Display a loading indicator: Without a well represented loading indicator (global dialog loader, standalone inline spinner...) the page will just freeze and the user can't tell if his request is being processed, or there was some sort of bug and the request isn't sent (and so he clicks again, and again and again). You need at least to disable the button or the form submission (or abort the previous request). Using the api we just defined, the loading indicator becomes easy to manage and can be reorganized to fit in several situations (icon button while disabling it, global loading dialog...)
  3. Display a clear and understandable error message: May be your user isn't connected his wifi anymore, but your application tab is always open and he clicks a button, then a network errors is thrown, so the user need to be aware that his request haven't reached your API, with the ability to retry (retry automatically is a plus). And in case of a real bug, a clear message need to be shown. This can be a part of the indicator component in the previously declared API (we use these techniques in production ;) ).
  4. Retrayable: The ability to retry a fetch request separately is a good feature and your users will surely need it without having to reload the whole window.
  5. Clean concurrency: Let's say your user is typing in a search bar, and because he's too fast, he sent two request (yes it is possible, even if debounced) to the API, but the last request resolved before the first. Yeah, you've guessed it right, if you do nothing about it, the search results of the first request will be displayed, and thus, wrong results. So, how to safely execute our callback and guarantee its concurrency's safety ? How to abort the previous requests ? is there any valid use case of requests cancellation ? The answer is simple: we should detect when our component did unmount or the dependencies have changed, and prevent the callbacks from being executed. (if the API you are working with allows you to abort a fetch request, aborting it is a plus).
  6. Back-button safe: What happens when the user clicks on the back button after you've just submitted a form ? should it stay fulfilled or become empty ? What happens when you re-visit a previously fetched list with filter ? should you reset the search or display the cached data ? If you need to display the cached data, your search form need to be filled, and you need to update stale data.

Can't perform a React state update on an unmounted component

This is an error that you may have already encountered while working with React. It occurs when you try to update the state of an unmounted component.

// deep inside React
console.error(
  "Can't perform a React state update on an unmounted component. This " +
    'is a no-op, but it indicates a memory leak in your application. To ' +
    'fix, cancel all subscriptions and asynchronous tasks in %s.',
  tag === ClassComponent
    ? 'the componentWillUnmount method'
    : 'a useEffect cleanup function',
);

I have seen this error so many times in the dev tools that I lost the count. It indicates that you have a memory leak in your app (well, React could've just ignore it, but it is good to follow the mental model: clean your subscription).

When fetching data, and you have a success/error callback that sets state, you need to protect it:

// this is a simple a naive way to handle the problem!
React.useEffect(
	function() {
        // this code is not production ready!!
        let didCancel = false;
        fetchData(
            requestData, 
            function onSuccess(data) {
            	if (!didCancel) setState(data);
            }, 
            function onError(error){
            	if (!didCancel) setError(error);
            }
        );
        return () => { didCancel = true; }
    },
    [dependencies]
);

Explanation:

  • React UseEffect will register an effect with cleanup
  • During the cleanup, we change the value of the DidCancel variable
  • The variable DidCancel being accessible in our success and error callbacks, by mutating it, we can prevent the call to our set state.

How about creating a useSafeCallback(callback, dependencies) hook that prevents our callback from being executed when dependencies change ? Well, I leave this as a good exercise for you. If you really need it and you did not manage to create it, just reach out to me or check the codesandbox examples with this article.

User input optimizations (debounce and throttle)

Batching requests and/or limiting how many times they can be invoked in a period of time can be very handy if you want to perform some data fetch.

1. Debounce

Debounced functions aren't executed right when you call them, but they wait a short period of time (delay). If the same function is called again, the wait delay is re-initialized.

This means, if the user types in a search bar, and we fix a delay of 400ms, we won't trigger any fetch request until he stops typing for 400ms.

Read more: useDebounce, freecodecamp link.

2. Throttle

Throttled function are executed once in a fixed duration.

If we take the same example of a user typing in a search bar, with a duration of 400ms. This means each 400ms we trigger (if called obviously) the fetch request even if the user is still typing.

These two principles can optimize performance not only related to data fetch, but to ui engineering in general.

Read more: Simple implementation, another link.

Conclusion

Please note that we already shipped the discussed aspects in this article to production, in many high scalable enterprise apps.

By now, you should have mastered the basics of data fetching in React, and you are supposed to deliver high quality applications.

As promised, here is the codesandbox link containing the code.

Mohamed EL AYADI

Mohamed EL AYADI

Hi, I'm Mohamed and I am a software engineer. I like to build efficient and secure apps. I am a java and javascript developer.

Tags:

signature

Mohamed EL AYADI has no other posts

Aloha from xHub team πŸ€™

We are glad you are here. Sharing is at the heart of our core beliefs as we like to spread the coding culture into the developer community, our blog will be your next IT info best friend, you will be finding logs about the latest IT trends tips and tricks, and more

Never miss a thing Subscribe for more content!

πŸ’Ό Offices

We’re remote friendly, with office locations around the world:
🌍 Casablanca, Agadir, Valencia, Quebec

πŸ“ž Contact Us:

🀳🏻 Follow us:

Β© XHUB. All rights reserved.

Made with πŸ’œ by xHub

Terms of Service