Menu

Building a Contact List with React MUI List: Complete Implementation Guide

As a front-end developer, you'll often need to display collections of data in a clean, interactive format. Contact lists are a common UI pattern, and Material UI's List component provides an excellent foundation for building them. In this article, I'll walk you through creating a fully functional contact list with selection capabilities and avatars using MUI's List components.

Learning Objectives

By the end of this guide, you'll be able to:

  • Implement a contact list with MUI's List component family
  • Add selection functionality (single and multiple)
  • Incorporate avatars and secondary text
  • Handle user interactions including clicks and selections
  • Customize the appearance using MUI's styling system
  • Implement virtualization for performance with large lists
  • Add advanced features like search filtering and sorting

Understanding MUI's List Component System

Before diving into the implementation, let's understand the core components we'll be working with. MUI's List system consists of several specialized components that work together to create versatile list interfaces.

The List component in MUI is a container that renders a <ul> element by default and is designed to display a collection of related items. It works in conjunction with ListItem, ListItemText, ListItemAvatar, and other related components to create rich list interfaces.

Let's explore the key components we'll use:

Core List Components

  1. List: The container component that wraps all list items.
  2. ListItem: The individual item in a list.
  3. ListItemButton: A wrapper that adds button functionality to list items.
  4. ListItemText: Displays primary and secondary text within a list item.
  5. ListItemAvatar: Displays an avatar within a list item.
  6. Checkbox/Radio: For selection functionality.
  7. Avatar: Displays user images or initials.

Each of these components serves a specific purpose in building a comprehensive contact list interface. Let's examine their props and behaviors in detail.

List Component Deep Dive

The List component is the foundation of our contact list UI. It provides the container for all list items and handles layout and spacing.

List Component Props

PropTypeDefaultDescription
childrennode-The content of the component, normally ListItem elements.
componentelementType'ul'The component used for the root node.
denseboolfalseDecreases the padding to create a more compact list.
disablePaddingboolfalseRemoves padding from the list items.
subheadernode-The content of the subheader, normally ListSubheader.
sxobject-The system prop that allows defining system overrides.

ListItem Component Props

PropTypeDefaultDescription
alignItems'flex-start' | 'center''center'Controls the alignment of items within the list item.
buttonboolfalseIf true, the list item will be a button (deprecated - use ListItemButton).
denseboolfalseIf true, compact vertical padding is used.
disableGuttersboolfalseIf true, the left and right padding is removed.
dividerboolfalseIf true, a divider is displayed below the list item.
secondaryActionnode-The element to display at the end of the list item.
selectedboolfalseIf true, the list item will be selected.

ListItemButton Props

PropTypeDefaultDescription
alignItems'flex-start' | 'center''center'Controls the alignment of items.
autoFocusboolfalseIf true, the list item will be focused during the first mount.
denseboolfalseIf true, compact vertical padding is used.
disableGuttersboolfalseIf true, the left and right padding is removed.
dividerboolfalseIf true, a divider is displayed below the list item.
selectedboolfalseIf true, the list item will be selected.

ListItemText Props

PropTypeDefaultDescription
primarynode-The main text content.
primaryTypographyPropsobject-Props applied to the primary Typography element.
secondarynode-The secondary text content.
secondaryTypographyPropsobject-Props applied to the secondary Typography element.
insetboolfalseIf true, the children will be indented.

ListItemAvatar Props

PropTypeDefaultDescription
childrennode-The content of the component, normally an Avatar.

Setting Up the Project

Let's start by setting up a new React project and installing the necessary dependencies.

Creating a New React Project

First, we'll create a new React application using Create React App:

npx create-react-app contact-list-mui
cd contact-list-mui

Installing Material UI Dependencies

Next, we'll install Material UI and its icon package:

npm install @mui/material @mui/icons-material @emotion/react @emotion/styled

Now that we have our project set up, let's start building our contact list component.

Building a Basic Contact List

Let's start with a simple contact list that displays names and avatars.

Step 1: Create the Contact Data

First, we'll create some mock data for our contacts:

// src/data/contacts.js
const contacts = [
  {
    id: 1,
    name: "John Smith",
    email: "john.smith@example.com",
    phone: "+1 (555) 123-4567",
    avatar: "https://mui.com/static/images/avatar/1.jpg",
  },
  {
    id: 2,
    name: "Sarah Johnson",
    email: "sarah.johnson@example.com",
    phone: "+1 (555) 234-5678",
    avatar: "https://mui.com/static/images/avatar/2.jpg",
  },
  {
    id: 3,
    name: "Michael Brown",
    email: "michael.brown@example.com",
    phone: "+1 (555) 345-6789",
    avatar: "https://mui.com/static/images/avatar/3.jpg",
  },
  {
    id: 4,
    name: "Emily Davis",
    email: "emily.davis@example.com",
    phone: "+1 (555) 456-7890",
    avatar: "https://mui.com/static/images/avatar/4.jpg",
  },
  {
    id: 5,
    name: "Daniel Wilson",
    email: "daniel.wilson@example.com",
    phone: "+1 (555) 567-8901",
    avatar: "https://mui.com/static/images/avatar/5.jpg",
  },
];

export default contacts;

This data structure includes all the information we'll need for our contact list: unique IDs, names, contact information, and avatar URLs.

Step 2: Create a Basic Contact List Component

Now, let's create a basic contact list component that displays our contacts:

// src/components/BasicContactList.jsx
import React from 'react';
import { 
  List, 
  ListItem, 
  ListItemText, 
  ListItemAvatar, 
  Avatar,
  Paper,
  Typography
} from '@mui/material';
import contacts from '../data/contacts';

function BasicContactList() {
  return (
    <Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', mt: 4 }}>
      <Typography variant="h6" sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText' }}>
        Contacts
      </Typography>
      <List sx={{ width: '100%', bgcolor: 'background.paper' }}>
        {contacts.map((contact) => (
          <ListItem key={contact.id} divider>
            <ListItemAvatar>
              <Avatar alt={contact.name} src={contact.avatar} />
            </ListItemAvatar>
            <ListItemText
              primary={contact.name}
              secondary={contact.email}
            />
          </ListItem>
        ))}
      </List>
    </Paper>
  );
}

export default BasicContactList;

Let's update our App.js to display this component:

// src/App.js
import React from 'react';
import BasicContactList from './components/BasicContactList';
import { CssBaseline, Container } from '@mui/material';

function App() {
  return (
    <>
      <CssBaseline />
      <Container>
        <BasicContactList />
      </Container>
    </>
  );
}

export default App;

This gives us a simple contact list with avatars and basic contact information. Let's break down what's happening:

  1. We're using the List component as a container for our list items.
  2. Each contact is represented by a ListItem with a divider to separate entries.
  3. ListItemAvatar and Avatar components display the contact's image.
  4. ListItemText displays the contact's name as primary text and email as secondary text.
  5. We've wrapped everything in a Paper component with some styling for a clean look.

Adding Selection Functionality

Now, let's enhance our contact list by adding selection functionality. We'll implement both single and multiple selection modes.

Creating a Selectable Contact List

Let's create a new component for a selectable contact list:

// src/components/SelectableContactList.jsx
import React, { useState } from 'react';
import {
  List,
  ListItemButton,
  ListItemText,
  ListItemAvatar,
  Avatar,
  Paper,
  Typography,
  Checkbox,
  IconButton,
  Divider,
  Box,
  Switch,
  FormControlLabel
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import contacts from '../data/contacts';

function SelectableContactList() {
  const [selected, setSelected] = useState([]);
  const [multiSelect, setMultiSelect] = useState(false);

  const handleToggle = (id) => () => {
    if (multiSelect) {
      // For multiple selection
      const currentIndex = selected.indexOf(id);
      const newSelected = [...selected];

      if (currentIndex === -1) {
        newSelected.push(id);
      } else {
        newSelected.splice(currentIndex, 1);
      }

      setSelected(newSelected);
    } else {
      // For single selection
      setSelected(selected.includes(id) ? [] : [id]);
    }
  };

  const handleDeleteSelected = () => {
    console.log(`Deleting contacts: ${selected.join(', ')}`);
    // In a real app, you would call an API to delete these contacts
    setSelected([]);
  };

  const handleToggleMultiSelect = () => {
    setMultiSelect(!multiSelect);
    setSelected([]); // Clear selections when switching modes
  };

  return (
    <Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', mt: 4 }}>
      <Box sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Typography variant="h6">
          Contacts ({selected.length} selected)
        </Typography>
        {selected.length > 0 && (
          <IconButton 
            color="inherit" 
            onClick={handleDeleteSelected}
            aria-label="delete selected contacts"
          >
            <DeleteIcon />
          </IconButton>
        )}
      </Box>
      
      <Box sx={{ p: 1, bgcolor: 'background.paper' }}>
        <FormControlLabel
          control={
            <Switch
              checked={multiSelect}
              onChange={handleToggleMultiSelect}
              color="primary"
            />
          }
          label="Multiple selection"
        />
      </Box>
      
      <Divider />
      
      <List sx={{ width: '100%', bgcolor: 'background.paper' }}>
        {contacts.map((contact) => {
          const isSelected = selected.indexOf(contact.id) !== -1;
          
          return (
            <ListItemButton 
              key={contact.id} 
              divider
              selected={isSelected}
              onClick={handleToggle(contact.id)}
              dense
            >
              <Checkbox
                edge="start"
                checked={isSelected}
                tabIndex={-1}
                disableRipple
                inputProps={{ 'aria-labelledby': `checkbox-list-label-${contact.id}` }}
              />
              <ListItemAvatar>
                <Avatar alt={contact.name} src={contact.avatar} />
              </ListItemAvatar>
              <ListItemText
                id={`checkbox-list-label-${contact.id}`}
                primary={contact.name}
                secondary={contact.email}
              />
            </ListItemButton>
          );
        })}
      </List>
    </Paper>
  );
}

export default SelectableContactList;

Now, let's update our App.js to use this new component:

// src/App.js
import React from 'react';
import SelectableContactList from './components/SelectableContactList';
import { CssBaseline, Container } from '@mui/material';

function App() {
  return (
    <>
      <CssBaseline />
      <Container>
        <SelectableContactList />
      </Container>
    </>
  );
}

export default App;

Let's analyze the key aspects of our selectable contact list:

  1. State Management: We use React's useState hook to track selected contacts and the selection mode.
  2. Selection Handling: The handleToggle function manages both single and multiple selection modes.
  3. ListItemButton: We've replaced ListItem with ListItemButton to make the entire list item clickable.
  4. Checkbox Component: We've added checkboxes to indicate selection status.
  5. Visual Feedback: The selected prop on ListItemButton provides visual feedback for selected items.
  6. Action Buttons: We've added a delete button that appears when items are selected.
  7. Selection Mode Toggle: A switch allows users to toggle between single and multiple selection modes.

This implementation gives users the flexibility to select contacts in different ways based on their needs.

Advanced Contact List with Detailed Information

Let's create a more advanced contact list that displays additional information and provides more interaction options.

// src/components/AdvancedContactList.jsx
import React, { useState } from 'react';
import {
  List,
  ListItemButton,
  ListItemText,
  ListItemAvatar,
  ListItemSecondaryAction,
  Avatar,
  Paper,
  Typography,
  IconButton,
  Divider,
  Box,
  Collapse,
  TextField,
  InputAdornment,
  Chip
} from '@mui/material';
import {
  Phone as PhoneIcon,
  Email as EmailIcon,
  ExpandMore as ExpandMoreIcon,
  ExpandLess as ExpandLessIcon,
  Search as SearchIcon,
  Star as StarIcon,
  StarBorder as StarBorderIcon
} from '@mui/icons-material';
import contacts from '../data/contacts';

function AdvancedContactList() {
  const [expanded, setExpanded] = useState({});
  const [searchQuery, setSearchQuery] = useState('');
  const [favorites, setFavorites] = useState([]);

  const handleToggleExpand = (id) => {
    setExpanded(prev => ({
      ...prev,
      [id]: !prev[id]
    }));
  };

  const handleToggleFavorite = (id, event) => {
    event.stopPropagation(); // Prevent triggering the expand/collapse
    
    setFavorites(prev => {
      if (prev.includes(id)) {
        return prev.filter(favId => favId !== id);
      } else {
        return [...prev, id];
      }
    });
  };

  const handleSearch = (event) => {
    setSearchQuery(event.target.value);
  };

  // Filter contacts based on search query
  const filteredContacts = contacts.filter(contact => {
    const searchLower = searchQuery.toLowerCase();
    return (
      contact.name.toLowerCase().includes(searchLower) ||
      contact.email.toLowerCase().includes(searchLower) ||
      contact.phone.includes(searchQuery)
    );
  });

  // Sort contacts: favorites first, then alphabetically
  const sortedContacts = [...filteredContacts].sort((a, b) => {
    // First sort by favorite status
    const aFav = favorites.includes(a.id) ? 1 : 0;
    const bFav = favorites.includes(b.id) ? 1 : 0;
    
    if (bFav !== aFav) {
      return bFav - aFav; // Favorites first
    }
    
    // Then sort alphabetically
    return a.name.localeCompare(b.name);
  });

  return (
    <Paper elevation={3} sx={{ maxWidth: 500, mx: 'auto', mt: 4 }}>
      <Box sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText' }}>
        <Typography variant="h6">
          Contact Directory
        </Typography>
      </Box>
      
      <Box sx={{ p: 2, bgcolor: 'background.paper' }}>
        <TextField
          fullWidth
          placeholder="Search contacts..."
          variant="outlined"
          size="small"
          value={searchQuery}
          onChange={handleSearch}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start">
                <SearchIcon />
              </InputAdornment>
            ),
          }}
        />
      </Box>
      
      <Divider />
      
      {favorites.length > 0 && (
        <Box sx={{ p: 1, bgcolor: 'background.paper' }}>
          <Typography variant="subtitle2" color="text.secondary">
            Favorites: {favorites.length}
          </Typography>
        </Box>
      )}
      
      <List sx={{ width: '100%', bgcolor: 'background.paper' }}>
        {sortedContacts.length === 0 ? (
          <Box sx={{ p: 2, textAlign: 'center' }}>
            <Typography color="text.secondary">No contacts found</Typography>
          </Box>
        ) : (
          sortedContacts.map((contact) => {
            const isExpanded = expanded[contact.id] || false;
            const isFavorite = favorites.includes(contact.id);
            
            return (
              <React.Fragment key={contact.id}>
                <ListItemButton 
                  onClick={() => handleToggleExpand(contact.id)}
                  sx={{
                    bgcolor: isFavorite ? 'rgba(255, 215, 0, 0.1)' : 'inherit',
                  }}
                >
                  <ListItemAvatar>
                    <Avatar alt={contact.name} src={contact.avatar} />
                  </ListItemAvatar>
                  <ListItemText
                    primary={
                      <Box sx={{ display: 'flex', alignItems: 'center' }}>
                        {contact.name}
                        {isFavorite && (
                          <Chip 
                            size="small" 
                            label="Favorite" 
                            color="primary" 
                            sx={{ ml: 1, height: 20 }}
                          />
                        )}
                      </Box>
                    }
                    secondary={contact.email}
                  />
                  <ListItemSecondaryAction>
                    <IconButton 
                      edge="end" 
                      onClick={(e) => handleToggleFavorite(contact.id, e)}
                      color={isFavorite ? "primary" : "default"}
                    >
                      {isFavorite ? <StarIcon /> : <StarBorderIcon />}
                    </IconButton>
                    {isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
                  </ListItemSecondaryAction>
                </ListItemButton>
                
                <Collapse in={isExpanded} timeout="auto" unmountOnExit>
                  <Box sx={{ p: 2, pl: 9, bgcolor: 'action.hover' }}>
                    <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
                      <EmailIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />
                      <Typography variant="body2">{contact.email}</Typography>
                    </Box>
                    <Box sx={{ display: 'flex', alignItems: 'center' }}>
                      <PhoneIcon fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />
                      <Typography variant="body2">{contact.phone}</Typography>
                    </Box>
                  </Box>
                </Collapse>
                
                <Divider />
              </React.Fragment>
            );
          })
        )}
      </List>
    </Paper>
  );
}

export default AdvancedContactList;

Update our App.js to use this advanced component:

// src/App.js
import React from 'react';
import AdvancedContactList from './components/AdvancedContactList';
import { CssBaseline, Container } from '@mui/material';

function App() {
  return (
    <>
      <CssBaseline />
      <Container>
        <AdvancedContactList />
      </Container>
    </>
  );
}

export default App;

This advanced contact list includes several sophisticated features:

  1. Expandable Details: Users can click on contacts to expand and see more details.
  2. Search Functionality: Users can search for contacts by name, email, or phone number.
  3. Favorites: Users can mark contacts as favorites, which are displayed at the top of the list.
  4. Visual Indicators: Icons and color changes indicate expanded and favorite status.
  5. Empty State Handling: A message is displayed when no contacts match the search criteria.

The implementation demonstrates how to combine various MUI components to create a rich, interactive interface.

Building a Virtualized Contact List for Performance

When dealing with large lists, performance can become an issue. MUI doesn't include virtualization out of the box, but we can use react-window to implement it. Let's create a virtualized contact list:

First, install the required package:

npm install react-window

Now, let's create the virtualized component:

// src/components/VirtualizedContactList.jsx
import React, { useState } from 'react';
import { FixedSizeList } from 'react-window';
import {
  ListItemButton,
  ListItemText,
  ListItemAvatar,
  Avatar,
  Paper,
  Typography,
  Checkbox,
  Box,
  Divider
} from '@mui/material';

// Generate a large number of contacts for demonstration
const generateLargeContactList = (count) => {
  return Array.from({ length: count }, (_, index) => ({
    id: index + 1,
    name: `Contact ${index + 1}`,
    email: `contact${index + 1}@example.com`,
    phone: `+1 (555) ${100 + index}-${1000 + index}`,
    avatar: `https://mui.com/static/images/avatar/${(index % 8) + 1}.jpg`,
  }));
};

const largeContactList = generateLargeContactList(1000);

function VirtualizedContactList() {
  const [selected, setSelected] = useState([]);

  const handleToggle = (id) => {
    const currentIndex = selected.indexOf(id);
    const newSelected = [...selected];

    if (currentIndex === -1) {
      newSelected.push(id);
    } else {
      newSelected.splice(currentIndex, 1);
    }

    setSelected(newSelected);
  };

  // Render an individual row
  const renderRow = ({ index, style }) => {
    const contact = largeContactList[index];
    const isSelected = selected.indexOf(contact.id) !== -1;

    return (
      <ListItemButton
        style={style}
        selected={isSelected}
        onClick={() => handleToggle(contact.id)}
        divider
      >
        <Checkbox
          edge="start"
          checked={isSelected}
          tabIndex={-1}
          disableRipple
        />
        <ListItemAvatar>
          <Avatar alt={contact.name} src={contact.avatar} />
        </ListItemAvatar>
        <ListItemText
          primary={contact.name}
          secondary={contact.email}
        />
      </ListItemButton>
    );
  };

  return (
    <Paper elevation={3} sx={{ maxWidth: 500, mx: 'auto', mt: 4 }}>
      <Box sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText' }}>
        <Typography variant="h6">
          Large Contact List ({largeContactList.length} contacts)
        </Typography>
      </Box>
      
      <Divider />
      
      <Box sx={{ height: 400, width: '100%' }}>
        <FixedSizeList
          height={400}
          width="100%"
          itemSize={72} // Height of each row
          itemCount={largeContactList.length}
          overscanCount={5} // Number of items to render outside of the visible area
        >
          {renderRow}
        </FixedSizeList>
      </Box>
    </Paper>
  );
}

export default VirtualizedContactList;

Update our App.js to use this virtualized component:

// src/App.js
import React from 'react';
import VirtualizedContactList from './components/VirtualizedContactList';
import { CssBaseline, Container } from '@mui/material';

function App() {
  return (
    <>
      <CssBaseline />
      <Container>
        <VirtualizedContactList />
      </Container>
    </>
  );
}

export default App;

The virtualized list offers significant performance improvements when working with large datasets:

  1. Efficient Rendering: Only the visible items (and a few extra for smooth scrolling) are actually rendered in the DOM.
  2. Reduced Memory Usage: Since fewer DOM elements are created, memory usage is reduced.
  3. Smooth Scrolling: The virtualization library handles efficient updates during scrolling.

This approach is crucial for maintaining good performance when dealing with hundreds or thousands of contacts.

Creating a Complete Contact Management Application

Let's combine everything we've learned to create a comprehensive contact management application with tabs for different views:

// src/components/ContactManagementApp.jsx
import React, { useState } from 'react';
import {
  Tabs,
  Tab,
  Box,
  Paper,
  Typography,
  List,
  ListItemButton,
  ListItemText,
  ListItemAvatar,
  ListItemSecondaryAction,
  Avatar,
  IconButton,
  TextField,
  InputAdornment,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
  Divider,
  Checkbox,
  Snackbar,
  Alert
} from '@mui/material';
import {
  Add as AddIcon,
  Delete as DeleteIcon,
  Edit as EditIcon,
  Search as SearchIcon,
  Star as StarIcon,
  StarBorder as StarBorderIcon,
  Person as PersonIcon
} from '@mui/icons-material';
import contacts from '../data/contacts';

function ContactManagementApp() {
  // State
  const [value, setValue] = useState(0);
  const [contactList, setContactList] = useState(contacts);
  const [searchQuery, setSearchQuery] = useState('');
  const [selected, setSelected] = useState([]);
  const [favorites, setFavorites] = useState([]);
  const [openDialog, setOpenDialog] = useState(false);
  const [currentContact, setCurrentContact] = useState(null);
  const [snackbar, setSnackbar] = useState({ open: false, message: '', severity: 'success' });

  // Form state
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    phone: '',
    avatar: ''
  });

  // Handlers
  const handleTabChange = (event, newValue) => {
    setValue(newValue);
  };

  const handleSearch = (event) => {
    setSearchQuery(event.target.value);
  };

  const handleToggleSelect = (id) => {
    const currentIndex = selected.indexOf(id);
    const newSelected = [...selected];

    if (currentIndex === -1) {
      newSelected.push(id);
    } else {
      newSelected.splice(currentIndex, 1);
    }

    setSelected(newSelected);
  };

  const handleToggleFavorite = (id, event) => {
    if (event) event.stopPropagation();
    
    setFavorites(prev => {
      if (prev.includes(id)) {
        return prev.filter(favId => favId !== id);
      } else {
        return [...prev, id];
      }
    });
  };

  const handleDeleteSelected = () => {
    setContactList(prev => prev.filter(contact => !selected.includes(contact.id)));
    setSelected([]);
    showSnackbar('Contacts deleted successfully', 'success');
  };

  const handleOpenDialog = (contact = null) => {
    if (contact) {
      setCurrentContact(contact);
      setFormData({
        name: contact.name,
        email: contact.email,
        phone: contact.phone,
        avatar: contact.avatar
      });
    } else {
      setCurrentContact(null);
      setFormData({
        name: '',
        email: '',
        phone: '',
        avatar: ''
      });
    }
    setOpenDialog(true);
  };

  const handleCloseDialog = () => {
    setOpenDialog(false);
  };

  const handleFormChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSaveContact = () => {
    if (currentContact) {
      // Edit existing contact
      setContactList(prev => 
        prev.map(contact => 
          contact.id === currentContact.id 
            ? { ...contact, ...formData } 
            : contact
        )
      );
      showSnackbar('Contact updated successfully', 'success');
    } else {
      // Add new contact
      const newContact = {
        id: Date.now(), // Simple way to generate unique ID
        ...formData
      };
      setContactList(prev => [...prev, newContact]);
      showSnackbar('Contact added successfully', 'success');
    }
    setOpenDialog(false);
  };

  const showSnackbar = (message, severity) => {
    setSnackbar({ open: true, message, severity });
  };

  const handleCloseSnackbar = () => {
    setSnackbar({ ...snackbar, open: false });
  };

  // Filter contacts based on search and current tab
  const filteredContacts = contactList.filter(contact => {
    const matchesSearch = searchQuery === '' || 
      contact.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
      contact.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
      contact.phone.includes(searchQuery);
    
    if (value === 0) return matchesSearch; // All contacts
    if (value === 1) return matchesSearch && favorites.includes(contact.id); // Favorites only
    return false;
  });

  return (
    <Paper elevation={3} sx={{ maxWidth: 600, mx: 'auto', mt: 4 }}>
      {/* Header */}
      <Box sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Typography variant="h6">
          Contact Manager
        </Typography>
        <Box>
          {selected.length > 0 ? (
            <IconButton 
              color="inherit" 
              onClick={handleDeleteSelected}
              aria-label="delete selected contacts"
            >
              <DeleteIcon />
            </IconButton>
          ) : (
            <IconButton 
              color="inherit" 
              onClick={() => handleOpenDialog()}
              aria-label="add new contact"
            >
              <AddIcon />
            </IconButton>
          )}
        </Box>
      </Box>

      {/* Tabs */}
      <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
        <Tabs value={value} onChange={handleTabChange} aria-label="contact tabs">
          <Tab label="All Contacts" id="tab-0" aria-controls="tabpanel-0" />
          <Tab label="Favorites" id="tab-1" aria-controls="tabpanel-1" />
        </Tabs>
      </Box>

      {/* Search */}
      <Box sx={{ p: 2 }}>
        <TextField
          fullWidth
          placeholder="Search contacts..."
          variant="outlined"
          size="small"
          value={searchQuery}
          onChange={handleSearch}
          InputProps={{
            startAdornment: (
              <InputAdornment position="start">
                <SearchIcon />
              </InputAdornment>
            ),
          }}
        />
      </Box>

      <Divider />

      {/* Contact List */}
      <Box role="tabpanel" id={`tabpanel-${value}`} aria-labelledby={`tab-${value}`}>
        <List sx={{ width: '100%', bgcolor: 'background.paper', maxHeight: 400, overflow: 'auto' }}>
          {filteredContacts.length === 0 ? (
            <Box sx={{ p: 3, textAlign: 'center' }}>
              <Typography color="text.secondary">
                {value === 0 
                  ? 'No contacts found. Add a new contact to get started.' 
                  : 'No favorite contacts found.'}
              </Typography>
            </Box>
          ) : (
            filteredContacts.map((contact) => {
              const isSelected = selected.includes(contact.id);
              const isFavorite = favorites.includes(contact.id);
              
              return (
                <ListItemButton 
                  key={contact.id}
                  selected={isSelected}
                  onClick={() => handleToggleSelect(contact.id)}
                  divider
                >
                  <Checkbox
                    edge="start"
                    checked={isSelected}
                    tabIndex={-1}
                    disableRipple
                    onClick={(e) => e.stopPropagation()}
                    onChange={() => handleToggleSelect(contact.id)}
                  />
                  <ListItemAvatar>
                    <Avatar 
                      alt={contact.name} 
                      src={contact.avatar}
                      sx={{ bgcolor: !contact.avatar ? 'primary.main' : undefined }}
                    >
                      {!contact.avatar && <PersonIcon />}
                    </Avatar>
                  </ListItemAvatar>
                  <ListItemText
                    primary={contact.name}
                    secondary={
                      <>
                        <Typography component="span" variant="body2" color="text.primary">
                          {contact.email}
                        </Typography>
                        {" — "}{contact.phone}
                      </>
                    }
                  />
                  <ListItemSecondaryAction>
                    <IconButton 
                      edge="end" 
                      onClick={(e) => handleToggleFavorite(contact.id, e)}
                      color={isFavorite ? "primary" : "default"}
                    >
                      {isFavorite ? <StarIcon /> : <StarBorderIcon />}
                    </IconButton>
                    <IconButton 
                      edge="end" 
                      onClick={(e) => {
                        e.stopPropagation();
                        handleOpenDialog(contact);
                      }}
                    >
                      <EditIcon />
                    </IconButton>
                  </ListItemSecondaryAction>
                </ListItemButton>
              );
            })
          )}
        </List>
      </Box>

      {/* Add/Edit Contact Dialog */}
      <Dialog open={openDialog} onClose={handleCloseDialog} aria-labelledby="form-dialog-title">
        <DialogTitle id="form-dialog-title">
          {currentContact ? 'Edit Contact' : 'Add New Contact'}
        </DialogTitle>
        <DialogContent>
          <TextField
            autoFocus
            margin="dense"
            name="name"
            label="Name"
            type="text"
            fullWidth
            value={formData.name}
            onChange={handleFormChange}
            required
          />
          <TextField
            margin="dense"
            name="email"
            label="Email Address"
            type="email"
            fullWidth
            value={formData.email}
            onChange={handleFormChange}
            required
          />
          <TextField
            margin="dense"
            name="phone"
            label="Phone Number"
            type="tel"
            fullWidth
            value={formData.phone}
            onChange={handleFormChange}
            required
          />
          <TextField
            margin="dense"
            name="avatar"
            label="Avatar URL"
            type="url"
            fullWidth
            value={formData.avatar}
            onChange={handleFormChange}
            helperText="Leave empty to use initials"
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={handleCloseDialog} color="primary">
            Cancel
          </Button>
          <Button 
            onClick={handleSaveContact} 
            color="primary"
            disabled={!formData.name || !formData.email || !formData.phone}
          >
            Save
          </Button>
        </DialogActions>
      </Dialog>

      {/* Snackbar for notifications */}
      <Snackbar
        open={snackbar.open}
        autoHideDuration={6000}
        onClose={handleCloseSnackbar}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
      >
        <Alert onClose={handleCloseSnackbar} severity={snackbar.severity} sx={{ width: '100%' }}>
          {snackbar.message}
        </Alert>
      </Snackbar>
    </Paper>
  );
}

export default ContactManagementApp;

Update our App.js to use this comprehensive component:

// src/App.js
import React from 'react';
import ContactManagementApp from './components/ContactManagementApp';
import { CssBaseline, Container } from '@mui/material';

function App() {
  return (
    <>
      <CssBaseline />
      <Container>
        <ContactManagementApp />
      </Container>
    </>
  );
}

export default App;

This comprehensive contact management application includes:

  1. Tab Navigation: Allows users to switch between all contacts and favorites.
  2. CRUD Operations: Users can create, read, update, and delete contacts.
  3. Search Functionality: Users can search for contacts.
  4. Selection and Batch Operations: Users can select multiple contacts and delete them at once.
  5. Favorites Management: Users can mark contacts as favorites.
  6. Form Validation: Basic validation for required fields.
  7. Feedback System: Snackbars provide feedback on actions.

The application demonstrates how to combine various MUI components into a cohesive, feature-rich interface.

Customizing MUI List Appearance with Theming

MUI provides powerful theming capabilities that allow you to customize the appearance of components across your application. Let's explore how to customize our contact list using MUI's theming system:

// src/theme/theme.js
import { createTheme } from '@mui/material/styles';

const theme = createTheme({
  palette: {
    primary: {
      main: '#1976d2',
      light: '#4791db',
      dark: '#115293',
      contrastText: '#fff',
    },
    secondary: {
      main: '#dc004e',
      light: '#e33371',
      dark: '#9a0036',
      contrastText: '#fff',
    },
    background: {
      default: '#f5f5f5',
      paper: '#fff',
    },
  },
  components: {
    // Customize List component
    MuiList: {
      styleOverrides: {
        root: {
          padding: 0,
        },
      },
    },
    // Customize ListItem component
    MuiListItem: {
      styleOverrides: {
        root: {
          '&.Mui-selected': {
            backgroundColor: 'rgba(25, 118, 210, 0.08)',
          },
        },
      },
    },
    // Customize ListItemButton component
    MuiListItemButton: {
      styleOverrides: {
        root: {
          '&.Mui-selected': {
            backgroundColor: 'rgba(25, 118, 210, 0.08)',
            '&:hover': {
              backgroundColor: 'rgba(25, 118, 210, 0.12)',
            },
          },
          '&:hover': {
            backgroundColor: 'rgba(0, 0, 0, 0.04)',
          },
        },
      },
    },
    // Customize Avatar component
    MuiAvatar: {
      styleOverrides: {
        root: {
          width: 40,
          height: 40,
        },
      },
    },
    // Customize Divider component
    MuiDivider: {
      styleOverrides: {
        root: {
          margin: 0,
        },
      },
    },
  },
  typography: {
    h6: {
      fontWeight: 500,
    },
    subtitle1: {
      fontWeight: 500,
    },
    body2: {
      color: 'rgba(0, 0, 0, 0.6)',
    },
  },
});

export default theme;

Now, let's update our App.js to use this theme:

// src/App.js
import React from 'react';
import { ThemeProvider } from '@mui/material/styles';
import { CssBaseline, Container } from '@mui/material';
import ContactManagementApp from './components/ContactManagementApp';
import theme from './theme/theme';

function App() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Container>
        <ContactManagementApp />
      </Container>
    </ThemeProvider>
  );
}

export default App;

This theming approach offers several benefits:

  1. Consistent Styling: The theme ensures consistent styling across all instances of the components.
  2. Global Customization: You can change the appearance of all instances of a component without modifying each one individually.
  3. Maintainable Code: Centralizing style definitions makes it easier to maintain and update your application's appearance.
  4. Brand Alignment: You can align the component styles with your brand guidelines.

MUI's theming system is particularly powerful because it allows for deep customization while maintaining the component's functionality and accessibility features.

Accessibility Considerations for MUI Lists

Accessibility is a crucial aspect of modern web applications. Let's enhance our contact list to ensure it's accessible to all users:

// src/components/AccessibleContactList.jsx
import React, { useState } from 'react';
import {
  List,
  ListItemButton,
  ListItemText,
  ListItemAvatar,
  Avatar,
  Paper,
  Typography,
  Box,
  Divider,
  VisuallyHidden
} from '@mui/material';
import contacts from '../data/contacts';

// Custom component for visually hidden content
const VisuallyHidden = ({ children }) => (
  <Box sx={{
    border: 0,
    clip: 'rect(0 0 0 0)',
    height: '1px',
    margin: -1,
    overflow: 'hidden',
    padding: 0,
    position: 'absolute',
    width: '1px',
    whiteSpace: 'nowrap'
  }}>
    {children}
  </Box>
);

function AccessibleContactList() {
  const [selectedId, setSelectedId] = useState(null);

  const handleListItemClick = (id) => {
    setSelectedId(id);
  };

  return (
    <Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', mt: 4 }}>
      <Box 
        component="header" 
        sx={{ p: 2, bgcolor: 'primary.main', color: 'primary.contrastText' }}
        role="banner"
      >
        <Typography variant="h6" component="h1">
          Contacts
        </Typography>
      </Box>
      
      <Divider />
      
      <Box role="region" aria-label="Contact list">
        <List 
          sx={{ width: '100%', bgcolor: 'background.paper' }}
          aria-label="contacts"
          role="listbox"
        >
          {contacts.map((contact) => (
            <ListItemButton
              key={contact.id}
              selected={selectedId === contact.id}
              onClick={() => handleListItemClick(contact.id)}
              role="option"
              aria-selected={selectedId === contact.id}
              divider
            >
              <ListItemAvatar>
                <Avatar 
                  alt={contact.name} 
                  src={contact.avatar}
                  aria-hidden="true" // Avatar is decorative
                />
              </ListItemAvatar>
              <ListItemText
                primary={contact.name}
                secondary={
                  <>
                    <VisuallyHidden>Email: </VisuallyHidden>
                    {contact.email}
                  </>
                }
                primaryTypographyProps={{
                  'aria-label': `Contact name: ${contact.name}`
                }}
                secondaryTypographyProps={{
                  'aria-label': `Email: ${contact.email}`
                }}
              />
            </ListItemButton>
          ))}
        </List>
      </Box>
    </Paper>
  );
}

export default AccessibleContactList;

The accessibility enhancements include:

  1. ARIA Roles: Adding appropriate roles like listbox and option to clarify the component's purpose.
  2. ARIA States: Using aria-selected to indicate the selected state.
  3. Screen Reader Text: Adding context for screen readers with VisuallyHidden components.
  4. Semantic Structure: Using appropriate landmark roles like banner and region.
  5. Descriptive Labels: Providing clear labels for screen readers with aria-label.

These enhancements ensure that users with disabilities can effectively use our contact list, improving the overall user experience for everyone.

Best Practices and Common Issues

When working with MUI's List components, keep these best practices in mind:

Best Practices for MUI Lists

  1. Use the Right Components for the Job:

    • Use ListItemButton for clickable items
    • Use ListItemText for primary and secondary text
    • Use ListItemAvatar for avatars or icons
  2. Maintain Proper Nesting:

    • Always nest ListItem directly inside List
    • Place ListItemText, ListItemAvatar, etc. inside ListItem
  3. Handle Selection States Properly:

    • Use the selected prop for visual feedback
    • Maintain selection state in your component's state
    • Provide clear visual and accessibility cues for selection
  4. Optimize for Performance:

    • Use virtualization for long lists
    • Implement efficient filtering and sorting
    • Avoid unnecessary re-renders by using memoization
  5. Ensure Accessibility:

    • Provide proper ARIA attributes
    • Ensure keyboard navigation works
    • Test with screen readers

Common Issues and Solutions

  1. Issue: List items not clickable Solution: Use ListItemButton instead of ListItem for clickable items

  2. Issue: Secondary actions triggering main click handler Solution: Use event.stopPropagation() in secondary action handlers

  3. Issue: Poor performance with large lists Solution: Implement virtualization with react-window or similar libraries

  4. Issue: Inconsistent styling Solution: Use MUI's theming system for consistent styling

  5. Issue: Selection state not working correctly Solution: Ensure you're properly tracking selected items in state and applying the selected prop

Wrapping Up

In this comprehensive guide, we've explored how to build various types of contact lists using MUI's List components. We've covered everything from basic lists to advanced implementations with selection, virtualization, and comprehensive CRUD operations.

We started with the fundamentals of MUI's List component system and progressively built more complex interfaces. Along the way, we explored important concepts like state management, performance optimization, theming, and accessibility.

The MUI List component family provides a powerful foundation for building rich, interactive lists in your React applications. By combining these components with React's state management and MUI's styling system, you can create polished, performant user interfaces that meet your specific requirements.

Remember to consider performance for large lists, ensure accessibility for all users, and maintain a consistent design through theming. With these considerations in mind, you'll be able to build contact lists that are both functional and delightful to use.