Data Fetching with React Suspense

Data Fetching with React Suspense

·

13 min read

Introduction

  • What is React suspense?

  • Importance of data fetching in react apps

  • Fetching Data with React Suspense

  • Data Fetching in React Suspense with custom hooks

  • Example and How to create custom Hook

  • Error Boundaries

  • Importance of handling errors

  • Creating Error Boundary Components

  • Integrating Error Boundaries with Suspense Fallbacks

  • Example Code

  • Real world use cases

  • 1. Fetch on Render

  • 2. Fetch-then-Render

  • 3. Render-as-you-Fetch

  • Implementing Pagination with React Suspense

  • Adding Filter Functionality

  • Example

  • Best Practices and Performance Considerations

  • Disabling Suspense for slow networks

  • Code Splitting

  • Data-caching strategies

  • Profiling and optimization

  • React Suspense and Concurrent UI

  • What is the Concurrent Mode?

  • Advantages of using Concurrent UI with React Suspense

  • Example

  • Conclusion

Introduction

React Suspense is a built in react component that lets you display a fallback until its children have finished loading

<Suspense fallback={<Loading />}>
  <SomeComponent />
</Suspense>

React Suspense simplifies the handling of asynchronous data fetching and rendering in react apps thus helping the developer to enable them to declaratively specify loading states, error handling.

For almost all web and mobile applications, one of the most important aspects if data fetching from a remote server.

In React applications typically we have components that fetch the data by using API, then after fetching they process the data and after the processing is done the data is rendered on the screen

React Suspense provides a more intuitive and easy to maintain way to fetch and render data on screen.

Here is a simple example of React Suspense in action

  1. We are creating a custom hook to fetch the data from the server. We are calling it useDataFetch this
import { useState, useEffect } from "react";

function useDataFetch(url) {
  const [data, setData] = useState(null);
  const [searching, setSearching] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function getData() {
      try {
        const response = await fetch(url);
        const resultantData = await response.json();
        setData(resultantData);
      } catch (err) {
        setError(err);
      } finally {
        setSearching(false);
      }
    }
    getData();
  }, [url]);

  return { data, searching, error };
}

2. We are then using the data returned by the useDataFetch hook with React Suspense

import React, { Suspense } from 'react';
import { useFetch } from './useFetch';

function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`https://jsonplaceholder.typicode.com/users/${userId}`);

  if (loading) {
    throw new Promise(resolve => setTimeout(resolve, 1000));
  }

  if (error) {
    throw error;
  }

  return <div>{data.name}</div>;
}

function App({ userId }) {
  return (
    <div>
      <Suspense fallback={<div>Getting User Data </div>}>
        <UserProfile userId={userId} />
      </Suspense>
    </div>
  );
}

In this example we have a UserProfile Component that is getting the data from the useDataFetch custom hook

Fetching Data with React Suspense and Custom Hooks

  • To Fetch data using React Suspense we need to create custom hooks.

  • These custom hooks will allow us to suspend the components while the data is retrieved

  • React Suspense streamlines the loading states and error boundaries and makes for a more modular and declarative approach to data fetching in React suspense

Let us consider an example to better understand this

  1. Let us consider an Object with a name resource, which is responsible for data catching and suspense integration
const resource = {
  data: null,
  promise: null,
};

resource Object

2. Now, we need to create a function that would update the resource object whenever it gets the data

const fetchData = async (url) => {
  resource.promise = fetch(url)
    .then((response) => response.json())
    .then((data) => {
      resource.data = data;
      resource.promise = null;
    });

  throw resource.promise;
};

fetchData function

3. Lastly we create a custom hook that we will integrate with React Suspense

import { useState, useEffect } from "react";

function useDataFetch(url) {
  const [data, setData] = useState(resource.data);

  useEffect(() => {
    if (!resource.promise) {
      fetchData(url);
    }

    let promise = resource.promise;

    promise
      .then((data) => setData(data))
      .catch((error) => {
       //take care of the error here
      });
  }, [url]);

  if (resource.data) {
    return data;
  } else {
    throw resource.promise;
  }
}

When the useDataFetch hook is called the component will stop rendering on the screen and then only when the data is available the compoenent will start rendering again

Integrating Custom Hooks with Suspense

In this section we will integrate the custom react hook that we created called the useFetch into our component

import React, { Suspense } from "react";
import { useDataFetch } from "./useDataFetch";

function Post({ id }) {
  const post = useFetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

  return (
    <div>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Getting the data from React</h1>
      <Suspense fallback={<div>It is taking some time to get the data</div>}>
        <Post id={1} />
      </Suspense>
    </div>
  );
}

Error Boundaries: A Simple Example

The errors that happen in the front-end applications need to be handled gracefully.

Creating an error boundary component allows the developer to catch any errors thrown by the suspended component

let us look at an example to understand this better

import React from "react";

class ErrorBoundary extends React.Component {
  state = { error: null };

  static getDerivedStateFromError(error) {
    return { error };
  }

  componentDidCatch(error, errorInfo) {
    // you can handle the errors here
  }

  render() {
    const { error } = this.state;
    const { children, fallback } = this.props;

    if (error) {
      return fallback ? fallback(error) : null;
    }

    return children;
  }
}

We need to wrap the components that are likely to throw errors inside the ErrorBoundary component that we created to it to be able to catch the errors

function App() {
  return (
    <div>
      <h1>Fetching Data with React Suspense</h1>
      <ErrorBoundary fallback={(error) => <div>Error: {error.message}</div>}>
        <Suspense fallback={<div>Loading post...</div>}>
          <Post id={1} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}
  • Fetching data with react suspense takes creating custom hooks that fetch data and integrating them with components

  • These components then suspended during data fetching.

  • ErrorBoundries can be deployed to catch any error which might be thrown by the suspended components

Why Error handling is important

We need to application to perform even if problems come during run time.

  • We are looking to avoid issues like crashing, nondescriptive error messages, or unexpected behavior that would really frustrate the user

  • This article is brought to you by DeadSimpleChat, Chat API and SDK for your website and app

  • Better error handling results in developers being able to quickly resolve issues and users getting relevant messages as to why the app is not working if it has stopped working.

  • Enhances user experiences and makes the application more robust

Creating error-handling boundary components

ErrorBoundry components catch errors anywhere in its child components, logs those errors and show the fallback UI

Let us consider an example of how to create an error boundary component

import { useState, useEffect } from 'react';

function useErrorBoundary() {
  const [error, setError] = useState(null);

  const handleError = (err) => {
    setError(err);
  };

  useEffect(
    () =>
      function cleanup() {
        setError(null);
      },
    []
  );

  return [error, handleError];
}

Here we are creating an error boundary hook, next We will create a ErrorBoundry component below

import React from 'react';
import { useErrorBoundary } from './useErrorBoundary';

function ErrorBoundary({ children, fallback }) {
  const [error, handleError] = useErrorBoundary();

  if (error) {
    return fallback ? fallback(error) : null;
  }

  return (
    <React.ErrorBoundary onError={handleError}>{children}</React.ErrorBoundary>
  );
}

Integrating the Error boundry Hook with the react component and react suspense fallback

In this case, if the component fails then the suspense fallback will display an alternate UI to the user

import React, { Suspense } from 'react';
import { useFetch } from './useFetch';
import ErrorBoundary from './ErrorBoundary';

function UserProfile({ userId }) {
  const { data, error } = useFetch(`https://api.example.com/users/${userId}`);

  if (error) {
    throw error;
  }

  return <div>{data.name}</div>;
}

function App() {
  return (
    <div>
      <h1>Data Fetching with React Suspense</h1>
      <ErrorBoundary fallback={(error) => <div>Error: {error.message}</div>}>
        <Suspense fallback={<div>Loading user profile...</div>}>
          <UserProfile userId={1} />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
}

In the above example

  • The userProfile component has the job of fetching the data by using the useFetch custom hook

  • If there is some error in fetching the data then the suspended component throws an error.

  • Which is caught by the ErrorBoundry component and the suspense fallback display the alternate UI

  • This leads to better user experience and developer experience as well

Real World use-cases

Data fetch and render patterns

  1. Fetch on render

  2. fetch then render

  3. render as you fetch

Fetch-On-Render

In fetch on render, the component renders placeholder and loading states while requesting the data as they mount.

Data fetching starts as soon as the component is rendered and is blocked (that is placeholders are shown) until the data arrives

Let us look at the example

import React, { useState, useEffect } from "react";

function BlogPost({ postId }) {
  const [post, setPost] = useState(null);

  useEffect(() => {
    (async () => {
      const response = await fetch(
        `https://jsonplaceholder.typicode.com/posts/${postId}`
      );
      const postData = await response.json();
      setPost(postData);
    })();
  }, [postId]);

  if (!post) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </div>
  );
}

2. Fetch-then-render

In this render pattern, the component is only shown when all the data is available for render

here no placeholder or loading state is shown, this may cause initial render times to slow down

But it is preferable for some kinds of use cases

import React, { useState, useEffect } from "react";

function BlogPosts() {
  const [posts, setPosts] = useState(null);

  useEffect(() => {
    (async () => {
      const response = await fetch(
        "https://jsonplaceholder.typicode.com/posts?_limit=10"
      );
      const postData = await response.json();
      setPosts(postData);
    })();
  }, []);

  if (!posts) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

3. Render as you fetch

Here the data is rendered as soon as it is coming from the server. Here the data fetching and rendering are occurring at the same time

As soon as some of the data is available it is rendered by the component while waiting for the additional data from the server

This technique combines React Suspense with the custom hooks to better manage the async process

import React, { useState, useEffect } from "react";

function useFetch(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    setData(null);
    fetch(url)
      .then((response) => response.json())
      .then((result) => {
        setData(result);
      });
  }, [url]);

  if (data === null) {
    throw new Promise((resolve) => setTimeout(resolve, 2000));
  }

  return data;
}

function BlogPost({ postId }) {
  const post = useFetch(
    `https://jsonplaceholder.typicode.com/posts/${postId}`
  );

  return (
    <div>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Render as you fetch</h1>
      <React.Suspense fallback={<div>please wait while we are searching</div>}>
        <BlogPost postId={1} />
      </React.Suspense>
    </div>
  );
}

These are some of the real-world fetching patterns available in react suspense

You can use them according to the nature of your react application and the amount of data.

Implementing Pagination and filtering for React Suspense

Let us learn how to implement pagination and filtering with react suspense

We are going to combine custom hooks and react suspense components to implment pagination

Pagination

Let us create a custom hook named usePaginatedFetch which will retrive the data based of the below parameters

URL and

page

import { useState, useEffect } from "react";

function usePaginatedFetch(url, page) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);

    fetch(`${url}?_page=${page}&_limit=10`)
      .then((response) => response.json())
      .then((result) => {
        setData(result);
        setLoading(false);
      });
  }, [url, page]);

  if (loading) {
    throw new Promise((resolve) => setTimeout(resolve, 2000));
  }

  return data;
}

Next, we will create a new component and name that Posts component then we will use the usePaginatedFetch component to display the data

import React, { Suspense, useState } from "react";
import { usePaginatedFetch } from "./usePaginatedFetch";

function Posts({ page }) {
  const posts = usePaginatedFetch(
    "https://jsonplaceholder.typicode.com/posts",
    page
  );

  return (
    <div>
      {posts.map((post) => (
        <div key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </div>
      ))}
    </div>
  );
}

function App() {
  const [page, setPage] = useState(1);

  return (
    <div>
      <h1>React Suspense Pagination</h1>
      <Suspense fallback={<div>Please wait while we search for the page</div>}>
        <Posts page={page} />
      </Suspense>
      <button onClick={() => setPage((prevPage) => prevPage - 1)} disabled={page === 1}>
        older Page
      </button>
      <button onClick={() => setPage((prevPage) => prevPage + 1)}>Next Page</button>
    </div>
  );
}

Again let us revisit what we did in the above example

  • We created a custom hook usePaginatedData which would fetch the data from the server using the params URL and page`

  • The state of the fetched data and the loading status are maintained by the custom hook

  • In our code when the loading state is set to true then the react signals the component to skip rendering and wait for the data and the usePaginatedFetch sends a promise to fetch the data

  • Then we created the Posts component that takes a page as a prop and renders a list of posts based on the data that is returned by the usePaginatedData custom hook. Data is fetched from the remote server

  • And In the App Component we are wrapping the Post Component with Suspense so as to load the loading state

  • We have also created 2 button components to handle pagination.

Filters

Let us add the filter functionality. To do this we need to modify the usePaginatedFetch custom hook to accept a new params Object

Let us look at the code

function usePaginatedFetch(url, page, params) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);

  const queryParams = new URLSearchParams({
    ...params,
    _page: page,
    _limit: 20,
  });

  useEffect(() => {
    setLoading(true);

    fetch(`${url}?${queryParams}`)
      .then((response) => response.json())
      .then((result) => {
        setData(result);
        setLoading(false);
      });
  }, [url, page, params]);

  if (loading) {
    throw new Promise((resolve) => setTimeout(resolve, 2000));
  }

  return data;
}

Let us also update the App component to have an input field. that will filter the posts by the title

function App() {
  const [page, setPage] = useState(1);
  const [searchTerm, setSearchTerm] = useState("");

  const handleSearch = (event) => {
    setSearchTerm(event.target.value);
    setPage(1);
  };

  return (
    <div>
      <h1>React Suspense with Pagination and Filters</h1>
      <input
        type="text"
        placeholder="Search by title..."
        value={searchTerm}
        onChange={handleSearch}
      />
      <Suspense fallback={<div>Loading...</div>}>
        <Posts page={page} params={{ q: searchTerm }} />
      </Suspense>
      <button
        onClick={() => setPage((prevPage) => prevPage - 1)}
        disabled={page === 1}
      >
        Previous
      </button>
      <button onClick={() => setPage((prevPage) => prevPage + 1)}>Next</button>
    </div>
  );
}

Let us consider what we are doing the above example

  • We are modifying the usePaginatedFetch hook to accept a params Object

  • In the App component we have added an input field to allow users to search for a term

  • We have also created a handleSearch function to handle the searchTerm state whenever the input value changes that is whenever a user writes into the input field

  • We are passing the searchTerm into the params props as a query parameter to the post component

  • the usePaginatedFetch now includes the search term in the API request. This allows the server to filter the results using the search term

Best Practices and Performance Considerations

  1. Disabling Suspense for slow networks
  • When the app is on a slow network, it might be better to show a loading screen rather than waiting for a fallback UI.

  • You can disable suspense for slow networks by using the navigator.connection API to detect the user's network speed

function isSlowNetwork() {
  return (
    navigator.connection &&
    (["slow-2g", "2g"].includes(navigator.connection.effectiveType) ||
      navigator.connection.saveData)
  );
}

When we get to know that the app is on a slow network then we can conditionally render the component without the suspense wrapper

function App() {
  if (isSlowNetwork()) {
    return <Posts />;
  } else {
    return (
      <div>
        <h1>React Suspense tutorial</h1>
        <Suspense fallback={<div>Searching...</div>}>
          <Posts />
        </Suspense>
      </div>
    );
  }
}

Code Splitting

  • With code splitting, you can separate your application into smaller chunks.

  • These chunks can be loaded only when they are needed thus saving on resources

  • For example: loading the resources as the user navigates through the app

  • For code splitting, you can use React.lazy

import React, { lazy, Suspense } from "react";

const Posts = lazy(() => import("./Posts"));

function App() {
  return (
    <div>
      <h1>React Suspense with Code Splitting</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Posts />
      </Suspense>
    </div>
  );
}

Data-caching strategies

You can improve the performance of an application by caching the data fetched from the server.

Profiling and Optimizing

Using React dev tools we can profile and optimize the application

We can check the component renders and find out the performance bottlenecks.

Thus we can optimize the app using the React Dev tools

Need Chat API for your website or app

DeadSimpleChat is a Chat API provider

  • Add Scalable Chat to your app in minutes

  • 10 Million Online Concurrent users

  • 99.999% Uptime

  • Moderation features

  • 1-1 Chat

  • Group Chat

  • Fully Customizable

  • Chat API and SDK

  • Pre-Built Chat

Conclusion

In this article we learnt about React Suspense and how we can use React suspense to fetch data by creating a custom hook

I hope you liked the article Thank you for reading