2
Chapter 2

Fetching 101: Basics of Data Fetching in React

This chapter dives into the essentials of data fetching in React applications, starting from a simple user profile page. It highlights the initial steps of making API calls, dealing with network delays, and managing state with React's useEffect hook.

In this chapter, you will learn

  • Understanding and implementing basic data fetching using useEffect in React.
  • Exploring the impact of network delays on frontend performance.
  • Practical implementation of a user profile and friends list component with API integration.

  • Starting Simple in React Data Fetching

    Imagine a simple React application: a profile page where a logged-in user can view their profile. To achieve this, we need to fetch the user's information from an API using their ID.

    The API endpoint we'll use returns basic user information. It's designed to include a delay, allowing us to later examine how slow API responses can impact frontend performance.

    Consider this API call:

    curl http://localhost:1573/users/u1

    And the expected response:

    { "id": "u1", "name": "Juntao Qiu", "bio": "Developer, Educator, Author", "interests": [ "Technology", "Outdoors", "Travel" ] }

    In a typical React setup, we would handle this data fetching within a useEffect call. React triggers this effect after completing the initial render.

    Here's how you might implement this in src/profile.tsx:

    src/profile.tsx
    const Profile = ({ id }: { id: string }) => { const [user, setUser] = useState<User | undefined>(); useEffect(() => { const fetchUser = async () => { const data = await get<User>(`/users/${id}`); setUser(data); }; fetchUser(); }, [id]); return ( <div> {user && user.name} </div> ); };

    The get function is a straightforward wrapper around the native fetch. You can replace it with axios.get or any other preferred method.

    Here's the utility function in utils.ts:

    utils.ts
    const baseurl = "http://localhost:1573"; async function get<T>(url: string): Promise<T> { const response = await fetch(`${baseurl}${url}`); if (!response.ok) { throw new Error("Network response was not ok"); } return await response.json() as Promise<T>; } export { get };

    To render the Profile component, update App.tsx as follows:

    App.tsx
    import { Profile } from "./src/profile.tsx"; function App() { return ( <div className="max-w-3xl m-auto my-4 text-slate-800"> <h1 className="text-4xl py-4 mb-4 tracking-wider font-bold">Profile</h1> <div className="rounded border p-6 border-slate-300"> <Profile id="u1" /> </div> </div> ); } export default App;

    Visualizing the rendering sequence over time, it would look something like this diagram:

    Fetch and then render
    Fetch and then render

    When a user accesses a React application, a typical flow begins with the browser downloading the initial HTML. As it parses the HTML, it encounters links to resources like JS and CSS. This process involves downloading, parsing, and executing JS bundles, building the CSSOM, and so on.

    Note: HTML parsing is usually done in a streaming manner, meaning it starts as soon as some bytes are received rather than waiting for the entire HTML to download. For simplicity, our illustration assumes the entire HTML is downloaded before DOM construction. More details can be found in Rendering on the Web, Client-side rendering of HTML and interactivity, and Populating the page: how browsers work.

    As JavaScript executes, React begins rendering and manipulating the DOM, then triggers a network request through useEffect. It waits until data returns from the server before re-rendering with the new data.

    To enhance user experience, we can introduce a Spinner component during data loading and an Error component for handling issues like unresponsive backends or nonexistent users.

    With these additions, the Profile component now includes additional states:

    src/profile.tsx
    const Profile = ({ id }: { id: string }) => { const [loading, setLoading] = useState<boolean>(false); const [error, setError] = useState<Error | undefined>(); const [user, setUser] = useState<User | undefined>(); useEffect(() => { const fetchUser = async () => { try { setLoading(true); const data = await get<User>(`/users/${id}`); setUser(data); } catch (e) { setError(e as Error); } finally { setLoading(false); } }; fetchUser(); }, [id]); if (loading) { return <div>Loading...</div>; } if (error) { return <div>Something went wrong...</div>; } return ( <> {user && user.name} </> ); };

    This structure should be familiar if you've worked with React before.

    Next, let's add more than just the username. We'll create an About component to display user information, adding simple styles for visual appeal.

    For brevity, I'm omitting Tailwind CSS from the code snippets. You can view the full styled components in the corresponding code repository.

    src/about.tsx
    const About = ({ user }: { user: User }) => { return ( <div> <div> <img src={user.avatar} alt={`User ${user.name} Avatar`} /> </div> <div> <div>{user.name}</div> <p>{user.bio}</p> </div> </div> ); };

    When data is correctly fetched, it renders as shown:

    User basic information fetched and rendered
    User basic information fetched and rendered

    We now have a basic Profile page, retrieving data from a backend API intentionally delayed by 1.5 seconds.

    Implementing the User's Friends List

    Consider the user's friends list, typically stored in a separate table and accessed via a different API endpoint. For example, /users/<id>/friends. We'll fetch this data in a new component, Friends.

    src/friends.tsx
    const Friends = ({ id }: { id: string }) => { const [loading, setLoading] = useState<boolean>(false); const [users, setUsers] = useState<User[]>([]); useEffect(() => { const fetchFriends = async () => { setLoading(true); const data = await get<User>(`/users/${id}/friends`); setLoading(false); setUsers(data); }; fetchFriends(); }, [id]); if(loading) { return <div>Loading...</div> } 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 };

    The structure of the Friends component mirrors that of Profile: managing state, fetching data in useEffect, and rendering based on loading, error, and successful data retrieval states.

    We can incorporate Friends into the Profile component like any regular React component:

    src/profile.tsx
    const Profile = () => { //... return ( <> {user && <About user={user} />} <Friends id={id} /> </> ); }

    At first glance, this implementation seems fine. However, if you consider the time taken for each API call, you might spot a potential issue. What if the /friends API also takes 1.5 seconds to respond? The total time to display the full page would be 3 seconds.

    2

    You have Completed Chapter 2

    This chapter lays the groundwork for mastering network requests in React. It provides a practical example of fetching and rendering data, setting the stage for more advanced patterns and performance considerations in subsequent chapters.

    The next chapter will further explore complex data fetching scenarios, addressing performance challenges in frontend applications.

    © 2023