3
Chapter 3

Fetching Resources in Parallel

Chapter 3 tackles the challenge of optimizing network requests in React applications. It focuses on implementing parallel data fetching strategies to minimize the impact of the network waterfall effect, enhancing application performance and user experience.

In this chapter, you will learn

  • Understanding and mitigating the request waterfall effect.
  • Implementing parallel requests for efficiency.
  • Handling dependencies in network requests.

  • The first issue we encounter is with the rendering order. Initially, in the Profile component, useEffect triggers a network request. However, since data takes 1.5 seconds to return, we display a loading indicator in the meantime.

    Once the data arrives, we render the About section, and a similar process occurs in the Friends component. Here, useEffect initiates another network request, waiting for the data to return.

    Visualizing the request timeline, it looks like this:

    The request waterfall issue
    The request waterfall issue

    The process involves three renderings. After the first render, the page displays a loading... message while initiating the /users/u1 request. When the server responds, the About section is displayed. As Friends renders, lacking available data, it shows a loading... message in its section and sends out the /users/u1/friends request. Upon receiving this data, the third rendering occurs.

    The component tree of About + Friends
    The component tree of About + Friends

    This sequence might be obvious in our current setup, but consider more complex scenarios. Imagine the Friends component nested deeper in the component tree or used in different pages or sections. In such cases, identifying the problem by statically reading the code becomes challenging.

    The situation worsens with more nested components following the same useEffect + loading + error pattern, potentially leading to cumulative performance issues:

    Request waterfall could be even worse
    Request waterfall could be even worse

    Over time, as the component tree grows, the page becomes increasingly slower.

    However, one might wonder if initiating data fetching simultaneously could mitigate this wait time.

    Sending Requests in Parallel

    We can address this issue by sending parallel requests. In the Profile component, we can start both requests simultaneously using Promise.all, passing the fetched friends list to the Friends component:

    src/profile.tsx
    const Profile = ({ id }: { id: string }) => { //... const [user, setUser] = useState<User | undefined>(); const [friends, setFriends] = useState<User[]>([]); useEffect(() => { const fetchUserAndFriends = async () => { try { setLoading(true); const [user, friends] = await Promise.all([ get<User>(`/users/${id}`), get<User[]>(`/users/${id}/friends`), ]); setUser(user); setFriends(friends); } catch (e) { setError(e as Error); } finally { setLoading(false); } }; fetchUserAndFriends(); }, [id]); //... }; export { Profile };

    The Promise.all() static method accepts an iterable of promises, returning a single Promise. This promise resolves when all input promises fulfill (including for an empty iterable), resulting in an array of fulfillment values. If any input promises reject, the returned promise rejects with the first rejection reason.

    Consequently, we modify Friends into a presentational component, responding only to the passed users list, rather than making its own requests:

    src/friends.tsx
    const Friends = ({ users }: { users: User[] }) => { return ( <div> <h2>Friends</h2> <div> {users.map((user) => ( <div> <img src={`https://i.pravatar.cc/150?u=${user.id}`} alt={`User ${user.name} avatar`} /> <span>{user.name}</span> </div> ))} </div> </div> ); }; export { Friends };

    Now, the total wait time is reduced to max(1.5, 1.5) = 1.5 seconds, a significant improvement:

    Send requests in parallel
    Send requests in parallel

    The only remaining issue is the potential wait for the slower request in extreme cases. We'll accept this limitation for now and explore solutions in subsequent chapters.

    The only async component now is Profile
    The only async component now is Profile

    Request Dependency

    Parallel requests expedite the loading of independent data. However, some requests depend on others. For example, we might need to fetch user information first and use the interests array from the response to retrieve recommended feeds for the user. This sequential dependency necessitates a return to the initial approach.

    In the Feeds component, we define loading, error, and data states, and useEffect initiates network fetching after the initial render:

    src/feeds.tsx
    const Feeds = ({ category }: { category: string }) => { const [loading, setLoading] = useState<boolean>(false); const [feeds, setFeeds] = useState<Feed[]>([]); useEffect(() => { const fetchFeeds = async () => { setLoading(true); const data = await get(`/articles/${category}`); setLoading(false); setFeeds(data); }; fetchFeeds(); }, [category]); if (loading) { return <div>Loading...</div>; } return ( <div> <h2>Your Feeds</h2> <div> {feeds.map((feed) => ( <> <h3>{feed.title}</h3> <p>{feed.description}</p> </> ))} </div> </div> ); };

    In the Profile component, we include Feeds as follows:

    src/profile.tsx
    return ( <> {user && <About user={user} />} <Friends users={friends} /> {user && <Feeds category={user.interests[0]} />} </> );

    Initially, About and Friends load, and as soon as the user data is available, we use interests[0] to fetch feeds, potentially taking another second. The overall wait time amounts to max(1.5, 1.5) + 1 = 2.5 seconds.

    The UI after all components are rendered
    The UI after all components are rendered

    This approach combines parallel and sequential requests, yielding better performance than the initial method.

    The mix of parallel and sequential requests
    The mix of parallel and sequential requests

    The feeds request must wait for the completion of the previous two requests, displaying a large spinner in the interim. While this solution is functional, we must consider the runtime data requirements for each specific user.

    3

    You have Completed Chapter 3

    By mastering parallel requests and managing dependencies in network calls, this chapter sets the foundation for building faster and more responsive React applications. Join us as we continue to navigate the intricate world of advanced network patterns in React.

    In the next chapter, we explore further optimization strategies and delve into more complex scenarios of network fetching in React.

    © 2023