Muhi Logo Text
Work With MeAboutTestimonialsBlog

How to Create Skeleton Table Loaders with Material UI (MUI)

Learn how to trigger a skeleton table loading while fetching data with Material UI and React.

Last updated on March 25, 2023

react
mui
MUI Table Skeleton Loader

Many of us developers encountered the scenario where a data table stays empty and idle until data arrives from an API call and the table gets filled. Even if the API is fast, the approach of not adding a table loading placeholder makes the UI feels slow and laggy.

In this tutorial, we will learn how to replace table cells with skeleton loaders using MUI Skeleton and Table components. We will also use a real API call to make it more exciting!

Here is what the final code will look like:

If this is your first time working with MUI, please follow the installation instructions from the official documentation to get started.

Create a Basic MUI Table

MUI Table component is made of several parts that are required to create a basic table - TableContainer, TableHead, TableRow, TableCell, and TableBody.

For example, here is a table with a few headers and rows:

import React from "react";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";

export default function App() {
  return (
    <TableContainer component={Paper}>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>Name</TableCell>
            <TableCell>Email</TableCell>
            <TableCell>Phone</TableCell>
            <TableCell>Website</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          <TableRow>
            <TableCell>Leanne Graham</TableCell>
            <TableCell>Sincere@april.biz</TableCell>
            <TableCell>1-770-736-8031 x56442</TableCell>
            <TableCell>hildegard.org</TableCell>
          </TableRow>
          <TableRow>
            <TableCell>Ervin Howell</TableCell>
            <TableCell>Shanna@melissa.tv</TableCell>
            <TableCell>010-692-6593 x09125</TableCell>
            <TableCell>anastasia.net</TableCell>
          </TableRow>
        </TableBody>
      </Table>
    </TableContainer>
  );
}

More details on the Table component are available in the official Material UI documentation

Fetch Data from an API

We’re going to create a useFetch hook to load the data on the first component cycle by triggering a real API call with fake users using {JSON} Placeholder.

const useFetch = () => {
  const [data, setData] = useState();

  const fetchData = async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const newData = await response.json();
    setData(newData);
  };

  useEffect(() => {
    fetchData();
  }, []);

  return { data, fetchData };
};

The hook exposes two parts:

  1. data - for populating the table
  2. fetchData function - for refreshing the table when necessary.

Now let’s consume the useFetch hook in our main component and populate the data table.

import React, { useEffect, useState } from "react";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";

export default function App() {
  const { data } = useFetch();
  return (
    <TableContainer component={Paper}>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>Name</TableCell>
            <TableCell>Email</TableCell>
            <TableCell>Phone</TableCell>
            <TableCell>Website</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {data?.map((row) => (
            <TableRow key={row.name}>
              <TableCell component="th" scope="row">
                {row.name}
              </TableCell>
              <TableCell>{row.email}</TableCell>
              <TableCell>{row.phone}</TableCell>
              <TableCell>{row.website}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

const useFetch = () => {
  const [data, setData] = useState();

  const fetchData = async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const newData = await response.json();
    setData(newData);
  };

  useEffect(() => {
    fetchData();
  }, []);

  return { data, fetchData };
};

Using Skeleton Loaders

Now that we have the data table receiving and populating data let’s do the fun part!

There are multiple variants of the skeleton loading in Material UI, but we’re specifically interested in the text variant as it closely depicts the text data.

<Skeleton animation="wave" variant="text" />

We also added a wave style to animate from left to right while waiting.

Create a Row Loader Component

Let’s create a component that returns N number of row skeleton loading elements; N can be a parameter that the consumer passes.

const TableRowsLoader = ({ rowsNum }) => {
  return [...Array(rowsNum)].map((row, index) => (
    <TableRow key={index}>
      <TableCell component="th" scope="row">
        <Skeleton animation="wave" variant="text" />
      </TableCell>
      <TableCell>
        <Skeleton animation="wave" variant="text" />
      </TableCell>
      <TableCell>
        <Skeleton animation="wave" variant="text" />
      </TableCell>
      <TableCell>
        <Skeleton animation="wave" variant="text" />
      </TableCell>
    </TableRow>
  ));
};

Then, we’re going to make two modifications to the fetchData function:

  1. Add a two seconds timeout to demonstrate the loading part clearly.
  2. Set data to undefined at the beginning of the function; this will be used as an indicator to trigger the loading component.
const useFetch = () => {
  const [data, setData] = useState();

  const fetchData = async () => {
    setData();
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const newData = await response.json();
    setTimeout(() => setData(newData), 2000);
  };

  useEffect(() => {
    fetchData();
  }, []);

  return { data, fetchData };
};

Lastly, let’s consume the new Row Loader component we created and see the results in action!

import React, { useState, useEffect } from "react";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import { Box, Button, Skeleton } from "@mui/material";

export default function App() {
  const { data, fetchData } = useFetch();

  return (
    <Box>
      <Button onClick={fetchData} variant="contained">
        Fetch Data
      </Button>
      <TableContainer component={Paper}>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell>Name</TableCell>
              <TableCell>Email</TableCell>
              <TableCell>Phone</TableCell>
              <TableCell>Website</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {!data ? (
              <TableRowsLoader rowsNum={10} />
            ) : (
              data?.map((row) => (
                <TableRow key={row.name}>
                  <TableCell component="th" scope="row">
                    {row.name}
                  </TableCell>
                  <TableCell>{row.email}</TableCell>
                  <TableCell>{row.phone}</TableCell>
                  <TableCell>{row.website}</TableCell>
                </TableRow>
              ))
            )}
          </TableBody>
        </Table>
      </TableContainer>
    </Box>
  );
}

const TableRowsLoader = ({ rowsNum }) => {
  return [...Array(rowsNum)].map((row, index) => (
    <TableRow key={index}>
      <TableCell component="th" scope="row">
        <Skeleton animation="wave" variant="text" />
      </TableCell>
      <TableCell>
        <Skeleton animation="wave" variant="text" />
      </TableCell>
      <TableCell>
        <Skeleton animation="wave" variant="text" />
      </TableCell>
      <TableCell>
        <Skeleton animation="wave" variant="text" />
      </TableCell>
    </TableRow>
  ));
};

const useFetch = () => {
  const [data, setData] = useState();

  const fetchData = async () => {
    setData();
    const response = await fetch("https://jsonplaceholder.typicode.com/users");
    const newData = await response.json();
    setTimeout(() => setData(newData), 2000);
  };

  useEffect(() => {
    fetchData();
  }, []);

  return { data, fetchData };
};

We also added a “Fetch Data” button to re-fetch and illustrate the Table loading action again.

Summary

Skeleton loaders are an excellent option for loading content and improving the user experience. They’re particularly helpful for lazy loading independent components on the screen to enhance the speed to when users can first interact with elements.

Bye for now 👋

If you enjoyed this post, I regularly share similar content on Twitter. Follow me @muhimasri to stay up to date, or feel free to message me on Twitter if you have any questions or comments. I'm always open to discussing the topics I write about!

Recommended Reading

Dive into the essentials of utilizing MUI Grid to build responsive web layouts in React.

react
mui

Discussion

Upskill Your Frontend Development Techniques 🌟

Subscribe to stay up-to-date and receive quality front-end development tutorials straight to your inbox!

No spam, sales, or ads. Unsubscribe anytime you wish.

© 2024, Muhi Masri