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:
data
- for populating the tablefetchData
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:
- Add a two seconds timeout to demonstrate the loading part clearly.
- 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 👋