Menu

Building Responsive Product Grid Layouts with React MUI Grid v2

As a front-end developer working with React and Material UI, creating responsive grid layouts is a fundamental skill you'll need for nearly every project. When I first started working with MUI's Grid system years ago, I struggled with its nuances—especially when building product layouts that needed to look great on every device. Today, I'll walk you through using MUI Grid v2, the latest iteration of MUI's grid system, to build a responsive product grid that adapts beautifully across all screen sizes.

What You'll Learn in This Guide

In this comprehensive guide, we'll cover:

  • Understanding MUI Grid v2's core concepts and how it differs from Grid v1
  • Setting up a responsive product grid layout from scratch
  • Mastering responsive breakpoints for different screen sizes
  • Implementing advanced Grid v2 features like nested grids and spacing control
  • Optimizing your product grid for performance and accessibility
  • Troubleshooting common Grid v2 issues

By the end of this tutorial, you'll have built a professional-quality, responsive product grid layout that can serve as the foundation for e-commerce sites, portfolios, or any application that needs to display collections of items.

Understanding MUI Grid v2: The Foundation

MUI Grid v2 is a complete reimagining of the original Grid component. It was introduced to address limitations in the original implementation while providing more flexibility and better performance. Before we dive into code, let's understand the fundamentals.

Grid v2 is based on CSS Grid Layout, a powerful two-dimensional layout system that represents a significant improvement over the flexbox-based Grid v1. This shift allows for more complex layouts with less code and better performance. If you've used Grid v1 before, you'll need to adjust your thinking slightly, but the benefits are well worth it.

One key difference is that Grid v2 is available as a separate package, which helps keep bundle sizes down if you don't need it. This modular approach reflects MUI's commitment to performance and flexibility.

Installation and Basic Setup

Let's start by installing the necessary packages. You'll need both the core MUI package and the separate Grid v2 package.

npm install @mui/material @mui/system @mui/material-nextjs @emotion/react @emotion/styled
npm install @mui/material-next

Now, let's create a basic product grid layout to display a collection of products. We'll start with a simple implementation and then refine it.

import React from 'react';
import { Grid } from '@mui/material-next/Grid'; // Note we import from material-next
import { Card, CardContent, CardMedia, Typography, Box } from '@mui/material';

// Sample product data
const products = [
  {
    id: 1,
    name: 'Wireless Headphones',
    price: 99.99,
    image: 'https://source.unsplash.com/random/300x200/?headphones',
    description: 'Premium wireless headphones with noise cancellation.'
  },
  {
    id: 2,
    name: 'Smartphone',
    price: 799.99,
    image: 'https://source.unsplash.com/random/300x200/?smartphone',
    description: 'Latest model with high-resolution camera and fast processor.'
  },
  {
    id: 3,
    name: 'Laptop',
    price: 1299.99,
    image: 'https://source.unsplash.com/random/300x200/?laptop',
    description: 'Powerful laptop for professional use with long battery life.'
  },
  {
    id: 4,
    name: 'Smartwatch',
    price: 249.99,
    image: 'https://source.unsplash.com/random/300x200/?smartwatch',
    description: 'Track your fitness and stay connected with this smartwatch.'
  }
];

const ProductGrid = () => {
  return (
    <Box sx={{ flexGrow: 1, padding: 2 }}>
      <Grid container spacing={2}>
        {products.map((product) => (
          <Grid xs={12} sm={6} md={4} lg={3} key={product.id}>
            <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
              <CardMedia
                component="img"
                height="140"
                image={product.image}
                alt={product.name}
              />
              <CardContent sx={{ flexGrow: 1 }}>
                <Typography gutterBottom variant="h5" component="h2">
                  {product.name}
                </Typography>
                <Typography variant="body2" color="text.secondary">
                  {product.description}
                </Typography>
                <Typography variant="h6" color="primary" sx={{ mt: 2 }}>
                  ${product.price.toFixed(2)}
                </Typography>
              </CardContent>
            </Card>
          </Grid>
        ))}
      </Grid>
    </Box>
  );
};

export default ProductGrid;

In this basic implementation, we've created a product grid that:

  1. Uses the new Grid v2 component from '@mui/material-next/Grid'
  2. Creates a responsive layout that shows 1 product per row on extra-small screens, 2 on small screens, 3 on medium screens, and 4 on large screens
  3. Uses MUI Card components to display each product with an image, name, description, and price

This is a solid starting point, but let's dive deeper into Grid v2's capabilities to create a more refined product grid.

MUI Grid v2 Deep Dive

Component Props

Grid v2 comes with a rich set of props that give you precise control over your layout. Let's explore the most important ones:

PropTypeDefaultDescription
containerbooleanfalseIf true, the component will act as a grid container
spacingnumber | object0Defines the space between grid items
xs, sm, md, lg, xlnumber | boolean | 'auto' | objectfalseDefines the number of columns the grid item should span for different breakpoints
columnsnumber | object12Defines the total number of columns in the grid
rowSpacingnumber | object0Defines the vertical space between grid items
columnSpacingnumber | object0Defines the horizontal space between grid items
disableEqualOverflowbooleanfalseIf true, the negative margin that compensates for the spacing between items won't be applied
direction'row' | 'row-reverse' | 'column' | 'column-reverse''row'Defines the flex-direction of the grid container

One of the key differences between Grid v1 and v2 is that in v2, you don't need to specify the item prop for grid items. In v2, every direct child of a Grid container is automatically treated as a grid item.

Responsive Breakpoints

Grid v2 makes it easy to create responsive layouts by providing props that correspond to MUI's breakpoints:

  • xs: Extra-small screens (< 600px)
  • sm: Small screens (≥ 600px)
  • md: Medium screens (≥ 900px)
  • lg: Large screens (≥ 1200px)
  • xl: Extra-large screens (≥ 1536px)

For each breakpoint, you can specify how many columns a grid item should span. The grid is divided into 12 columns by default, so a value of 6 means the item will take up half the width of the container.

Let's see how to use these breakpoints to create a more responsive product grid:

<Grid container spacing={{ xs: 1, sm: 2, md: 3 }}>
  {products.map((product) => (
    <Grid xs={12} sm={6} md={4} lg={3} xl={2} key={product.id}>
      {/* Card content */}
    </Grid>
  ))}
</Grid>

This code creates a grid where:

  • On extra-small screens, each product takes up the full width (12/12 columns)
  • On small screens, two products per row (6/12 columns each)
  • On medium screens, three products per row (4/12 columns each)
  • On large screens, four products per row (3/12 columns each)
  • On extra-large screens, six products per row (2/12 columns each)

Additionally, we've used the object syntax for the spacing prop to apply different spacing at different breakpoints.

Customization

Grid v2 offers several ways to customize its appearance and behavior. Let's explore the most common methods:

Using the sx Prop

The sx prop is the most direct way to apply custom styles to MUI components. It allows you to use theme-aware values and responsive breakpoints.

<Grid 
  container 
  sx={{
    backgroundColor: 'background.paper',
    borderRadius: 1,
    p: { xs: 1, md: 2 },
    '& > .MuiGrid-item': {
      transition: 'transform 0.3s ease-in-out',
      '&:hover': {
        transform: 'translateY(-4px)'
      }
    }
  }}
>
  {/* Grid items */}
</Grid>

This example applies a paper background to the container, adds responsive padding, and includes a hover effect for all grid items.

Theme Customization

For global customization, you can modify the MUI theme to change how Grid v2 components look throughout your application:

import { createTheme, ThemeProvider } from '@mui/material/styles';

const theme = createTheme({
  components: {
    MuiGrid2: {
      styleOverrides: {
        root: {
          '&.MuiGrid2-container': {
            margin: '0 auto',
            maxWidth: '1200px',
          },
          '&.MuiGrid2-item': {
            padding: '16px',
          },
        },
      },
    },
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <ProductGrid />
    </ThemeProvider>
  );
}

Using the styled API

For more complex customizations, you can use the styled API to create customized versions of the Grid component:

import { styled } from '@mui/material/styles';
import { Grid } from '@mui/material-next/Grid';

const StyledGridItem = styled(Grid)(({ theme }) => ({
  '&:hover': {
    backgroundColor: theme.palette.action.hover,
  },
  padding: theme.spacing(2),
  transition: 'background-color 0.3s ease',
}));

// Then use it in your component
<Grid container>
  {products.map((product) => (
    <StyledGridItem xs={12} sm={6} md={4} lg={3} key={product.id}>
      {/* Card content */}
    </StyledGridItem>
  ))}
</Grid>

Limitations and Performance Considerations

While Grid v2 is powerful, it's important to be aware of its limitations:

  1. Deeply nested grids can impact performance. Try to keep your grid structure as flat as possible.
  2. Large numbers of grid items can cause performance issues. Consider implementing virtualization for long lists.
  3. Complex responsive behaviors might require custom CSS media queries in addition to the breakpoint props.

For best performance, follow these guidelines:

  • Use the container prop only when necessary
  • Avoid unnecessary nesting of Grid components
  • Consider using React.memo for grid items that don't change often
  • For very large lists, implement virtualization with libraries like react-virtualized or react-window

Building a Complete Responsive Product Grid

Now that we understand Grid v2's capabilities, let's build a more comprehensive product grid with advanced features. We'll create a product grid that includes:

  1. Responsive layout with different column counts based on screen size
  2. Filtering and sorting options
  3. Loading states and error handling
  4. Accessibility improvements

Step 1: Create the Basic Structure

First, let's set up our component structure:

import React, { useState } from 'react';
import { Grid } from '@mui/material-next/Grid';
import { 
  Card, CardContent, CardMedia, Typography, Box, 
  FormControl, InputLabel, Select, MenuItem, 
  TextField, CircularProgress, Alert, Button
} from '@mui/material';

const ProductGrid = ({ products: initialProducts, loading = false, error = null }) => {
  const [products, setProducts] = useState(initialProducts || []);
  const [sortBy, setSortBy] = useState('default');
  const [filterText, setFilterText] = useState('');
  
  // We'll implement these functions in the next steps
  const handleSort = (event) => {
    setSortBy(event.target.value);
  };
  
  const handleFilterChange = (event) => {
    setFilterText(event.target.value);
  };

  return (
    <Box sx={{ maxWidth: 1200, mx: 'auto', p: 2 }}>
      {/* Controls will go here */}
      
      {/* Products grid will go here */}
    </Box>
  );
};

export default ProductGrid;

Step 2: Add Control Elements

Now, let's add filtering and sorting controls:

// Inside the ProductGrid component
return (
  <Box sx={{ maxWidth: 1200, mx: 'auto', p: 2 }}>
    <Grid container spacing={2} sx={{ mb: 3 }}>
      <Grid xs={12} md={6}>
        <TextField
          fullWidth
          label="Search products"
          variant="outlined"
          value={filterText}
          onChange={handleFilterChange}
          disabled={loading}
        />
      </Grid>
      <Grid xs={12} md={6}>
        <FormControl fullWidth>
          <InputLabel id="sort-select-label">Sort by</InputLabel>
          <Select
            labelId="sort-select-label"
            value={sortBy}
            label="Sort by"
            onChange={handleSort}
            disabled={loading}
          >
            <MenuItem value="default">Default</MenuItem>
            <MenuItem value="priceLow">Price: Low to High</MenuItem>
            <MenuItem value="priceHigh">Price: High to Low</MenuItem>
            <MenuItem value="name">Name</MenuItem>
          </Select>
        </FormControl>
      </Grid>
    </Grid>
    
    {/* Products grid will go here */}
  </Box>
);

Step 3: Implement Filtering and Sorting Logic

Next, let's add the logic to filter and sort our products:

// Inside the ProductGrid component
// Filter and sort products
const getDisplayedProducts = () => {
  // First filter the products
  let filteredProducts = initialProducts;
  if (filterText) {
    const searchTerm = filterText.toLowerCase();
    filteredProducts = initialProducts.filter(
      product => 
        product.name.toLowerCase().includes(searchTerm) || 
        product.description.toLowerCase().includes(searchTerm)
    );
  }
  
  // Then sort them
  switch (sortBy) {
    case 'priceLow':
      return [...filteredProducts].sort((a, b) => a.price - b.price);
    case 'priceHigh':
      return [...filteredProducts].sort((a, b) => b.price - a.price);
    case 'name':
      return [...filteredProducts].sort((a, b) => a.name.localeCompare(b.name));
    default:
      return filteredProducts;
  }
};

const displayedProducts = getDisplayedProducts();

Step 4: Create the Product Grid with Loading and Error States

Now, let's implement the product grid with proper handling of loading and error states:

// Inside the ProductGrid component return statement, after the controls
{error && (
  <Alert severity="error" sx={{ mb: 2 }}>
    {error}
  </Alert>
)}

{loading ? (
  <Box sx={{ display: 'flex', justifyContent: 'center', my: 4 }}>
    <CircularProgress />
  </Box>
) : displayedProducts.length === 0 ? (
  <Alert severity="info" sx={{ mb: 2 }}>
    No products found matching your criteria.
  </Alert>
) : (
  <Grid 
    container 
    spacing={{ xs: 1, sm: 2, md: 3 }}
    columns={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 6 }}
  >
    {displayedProducts.map((product) => (
      <Grid xs={1} key={product.id}>
        <Card 
          sx={{ 
            height: '100%', 
            display: 'flex', 
            flexDirection: 'column',
            transition: 'transform 0.3s ease, box-shadow 0.3s ease',
            '&:hover': {
              transform: 'translateY(-4px)',
              boxShadow: 6
            }
          }}
        >
          <CardMedia
            component="img"
            height="200"
            image={product.image}
            alt={product.name}
            sx={{ objectFit: 'cover' }}
          />
          <CardContent sx={{ flexGrow: 1 }}>
            <Typography gutterBottom variant="h5" component="h2">
              {product.name}
            </Typography>
            <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
              {product.description}
            </Typography>
            <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 'auto' }}>
              <Typography variant="h6" color="primary">
                ${product.price.toFixed(2)}
              </Typography>
              <Button variant="contained" size="small">
                Add to Cart
              </Button>
            </Box>
          </CardContent>
        </Card>
      </Grid>
    ))}
  </Grid>
)}

Note the improvements in this implementation:

  1. We're using the columns prop to define the number of columns for each breakpoint
  2. We've added hover effects to the cards for better user interaction
  3. We've included proper loading and empty state handling
  4. We've added an "Add to Cart" button for each product

Step 5: Enhance Accessibility

Let's make our product grid more accessible:

// Inside the Card component
<Card
  sx={{
    height: '100%',
    display: 'flex',
    flexDirection: 'column',
    transition: 'transform 0.3s ease, box-shadow 0.3s ease',
    '&:hover': {
      transform: 'translateY(-4px)',
      boxShadow: 6
    },
    '&:focus-within': {  // Add keyboard focus styles
      outline: '2px solid',
      outlineColor: 'primary.main',
    }
  }}
>
  <CardMedia
    component="img"
    height="200"
    image={product.image}
    alt={product.name}  // Important for screen readers
    sx={{ objectFit: 'cover' }}
  />
  <CardContent sx={{ flexGrow: 1 }}>
    <Typography gutterBottom variant="h5" component="h2">
      {product.name}
    </Typography>
    <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
      {product.description}
    </Typography>
    <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 'auto' }}>
      <Typography variant="h6" color="primary">
        ${product.price.toFixed(2)}
      </Typography>
      <Button 
        variant="contained" 
        size="small"
        aria-label={`Add ${product.name} to cart`}  // Descriptive label for screen readers
      >
        Add to Cart
      </Button>
    </Box>
  </CardContent>
</Card>

Step 6: Add Performance Optimizations

For better performance, especially with large product lists, let's implement some optimizations:

import React, { useState, useMemo, memo } from 'react';

// Memoize the product card component to prevent unnecessary re-renders
const ProductCard = memo(({ product }) => {
  return (
    <Card
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        transition: 'transform 0.3s ease, box-shadow 0.3s ease',
        '&:hover': {
          transform: 'translateY(-4px)',
          boxShadow: 6
        },
        '&:focus-within': {
          outline: '2px solid',
          outlineColor: 'primary.main',
        }
      }}
    >
      {/* Card content as before */}
    </Card>
  );
});

// In the main component
const ProductGrid = ({ products: initialProducts, loading = false, error = null }) => {
  // State and handlers as before
  
  // Memoize the filtered and sorted products to prevent recalculation on every render
  const displayedProducts = useMemo(() => {
    // Filter and sort logic as before
    return getDisplayedProducts();
  }, [initialProducts, filterText, sortBy]);
  
  return (
    // JSX as before, but use the memoized ProductCard component
    <Grid container spacing={{ xs: 1, sm: 2, md: 3 }} columns={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 6 }}>
      {displayedProducts.map((product) => (
        <Grid xs={1} key={product.id}>
          <ProductCard product={product} />
        </Grid>
      ))}
    </Grid>
  );
};

Step 7: Implement Advanced Grid Features

Let's explore some advanced Grid v2 features to enhance our product grid:

// Creating a featured product section with different column spans
<Box sx={{ mb: 4 }}>
  <Typography variant="h4" component="h2" gutterBottom>
    Featured Products
  </Typography>
  <Grid 
    container 
    spacing={2}
    disableEqualOverflow // Prevents negative margins at the edges
  >
    {/* Featured product that spans multiple columns */}
    <Grid xs={12} md={8}>
      <Card sx={{ display: 'flex', height: '100%' }}>
        <Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
          <CardContent>
            <Typography component="h3" variant="h4">
              {featuredProduct.name}
            </Typography>
            <Typography variant="body1" paragraph>
              {featuredProduct.description}
            </Typography>
            <Typography variant="h5" color="primary">
              ${featuredProduct.price.toFixed(2)}
            </Typography>
            <Button variant="contained" size="large" sx={{ mt: 2 }}>
              View Details
            </Button>
          </CardContent>
        </Box>
        <CardMedia
          component="img"
          sx={{ width: { xs: '100%', md: '50%' }, display: { xs: 'none', md: 'block' } }}
          image={featuredProduct.image}
          alt={featuredProduct.name}
        />
      </Card>
    </Grid>
    
    {/* Secondary featured products */}
    <Grid xs={12} md={4} sx={{ display: 'flex', flexDirection: 'column' }}>
      <Grid container direction="column" spacing={2} sx={{ height: '100%' }}>
        <Grid xs>
          <Card sx={{ height: '100%' }}>
            {/* Secondary featured product 1 */}
          </Card>
        </Grid>
        <Grid xs>
          <Card sx={{ height: '100%' }}>
            {/* Secondary featured product 2 */}
          </Card>
        </Grid>
      </Grid>
    </Grid>
  </Grid>
</Box>

This implementation showcases:

  1. Using different column spans for featured products
  2. Nesting Grid containers for more complex layouts
  3. Using the disableEqualOverflow prop to prevent negative margins
  4. Responsive design that adapts to different screen sizes

Advanced Capabilities of Grid v2

Auto Layout

Grid v2 supports auto layout, which can be useful when you want the grid to automatically determine the width of items:

<Grid container spacing={2}>
  <Grid xs="auto">
    <Box sx={{ width: 100, height: 100, bgcolor: 'primary.main' }} />
  </Grid>
  <Grid xs>
    <Box sx={{ height: 100, bgcolor: 'secondary.main' }} />
  </Grid>
  <Grid xs={2}>
    <Box sx={{ height: 100, bgcolor: 'error.main' }} />
  </Grid>
</Grid>

In this example:

  • The first item (xs="auto") will be sized based on its content
  • The second item (xs) will fill the remaining space
  • The third item (xs={2}) will take up 2 columns

Custom Column Count

By default, Grid v2 uses a 12-column system, but you can customize this:

<Grid container columns={16} spacing={2}>
  <Grid xs={8}>
    <Box sx={{ height: 100, bgcolor: 'primary.main' }} />
  </Grid>
  <Grid xs={8}>
    <Box sx={{ height: 100, bgcolor: 'secondary.main' }} />
  </Grid>
</Grid>

This creates a 16-column grid with two items that each take up half the width.

Responsive Direction

You can change the direction of the grid based on screen size:

<Grid 
  container 
  spacing={2}
  direction={{ xs: 'column', sm: 'row' }}
>
  <Grid xs={12} sm={6}>
    <Box sx={{ height: 100, bgcolor: 'primary.main' }} />
  </Grid>
  <Grid xs={12} sm={6}>
    <Box sx={{ height: 100, bgcolor: 'secondary.main' }} />
  </Grid>
</Grid>

This grid will display items in a column on mobile and in a row on larger screens.

Different Row and Column Spacing

Grid v2 allows you to set different spacing for rows and columns:

<Grid 
  container 
  rowSpacing={3} // Vertical spacing
  columnSpacing={2} // Horizontal spacing
>
  {/* Grid items */}
</Grid>

This is particularly useful for creating layouts with different visual rhythms in horizontal and vertical directions.

Best Practices for MUI Grid v2

After working with Grid v2 on numerous projects, I've developed some best practices that will help you create more maintainable and performant layouts:

1. Keep Your Grid Structure Flat

Avoid deeply nesting Grid components when possible. Each level of nesting adds complexity and can impact performance. Instead, try to create layouts with a flat hierarchy.

2. Use Responsive Props Consistently

When defining breakpoints, be consistent with your approach. Either define all breakpoints for each item or use the cascading nature of the breakpoints (where values apply to the specified breakpoint and up).

// Good - explicit breakpoints
<Grid xs={12} sm={6} md={4} lg={3}>

// Also good - using cascading behavior
<Grid xs={12} sm={6} md={4}>

// Avoid - mixing approaches inconsistently
<Grid xs={12} md={4}> // Skipping sm can be confusing

3. Use Container Components Wisely

The container prop creates a new flex container. Only use it when you need to start a new grid system, not for every grouping of elements.

4. Leverage the sx Prop for One-off Styling

For component-specific styling that doesn't need to be reused, the sx prop provides a clean way to apply styles without creating custom styled components:

<Grid 
  xs={12} 
  md={6} 
  sx={{ 
    display: 'flex', 
    alignItems: 'center',
    '& img': { maxWidth: '100%' }
  }}
>
  <img src="product.jpg" alt="Product" />
</Grid>

5. Use Breakpoint Objects for Complex Responsive Behavior

For complex responsive behavior, use the object syntax for breakpoints:

<Grid 
  xs={{ span: 12, order: 2 }}
  md={{ span: 6, order: 1 }}
>
  {/* Content */}
</Grid>

This allows you to control multiple aspects of the grid item at each breakpoint.

Common Issues and Solutions

Issue 1: Unexpected Margins or Padding

Problem: Grid items have unexpected margins or padding, causing alignment issues.

Solution: Grid v2 applies negative margins to containers to compensate for item spacing. If you're seeing unexpected spacing, check:

  1. Make sure you're not adding custom margins to grid items
  2. Consider using the disableEqualOverflow prop if you don't want negative margins
  3. Use the rowSpacing and columnSpacing props for more precise control
<Grid 
  container 
  disableEqualOverflow 
  rowSpacing={2} 
  columnSpacing={2}
>
  {/* Grid items */}
</Grid>

Issue 2: Items Not Filling Available Height

Problem: Grid items within a row have different heights, and you want them all to be the same height.

Solution: Grid v2 doesn't automatically equalize heights. You need to ensure all items in a row have the same height:

<Grid container spacing={2}>
  {products.map((product) => (
    <Grid xs={12} sm={6} md={4} key={product.id}>
      <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
        <CardMedia
          component="img"
          height="140"
          image={product.image}
          alt={product.name}
          sx={{ objectFit: 'cover' }} // Ensure consistent image display
        />
        <CardContent sx={{ flexGrow: 1 }}> {/* This will expand to fill available space */}
          {/* Card content */}
        </CardContent>
      </Card>
    </Grid>
  ))}
</Grid>

Issue 3: Performance with Large Lists

Problem: Performance issues when rendering large product lists.

Solution: Implement virtualization for long lists:

import React, { useState } from 'react';
import { Grid } from '@mui/material-next/Grid';
import { Box } from '@mui/material';
import { FixedSizeGrid as VirtualGrid } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

const VirtualizedProductGrid = ({ products }) => {
  // Calculate columns based on viewport width
  const getColumnCount = (width) => {
    if (width < 600) return 1;
    if (width < 960) return 2;
    if (width < 1280) return 3;
    return 4;
  };

  const Cell = ({ columnIndex, rowIndex, style, data }) => {
    const { products, columnCount } = data;
    const index = rowIndex * columnCount + columnIndex;
    
    if (index >= products.length) return null;
    
    const product = products[index];
    
    return (
      <div style={style}>
        <Box sx={{ p: 1, height: '100%' }}>
          {/* Your product card component */}
          <Card sx={{ height: '100%' }}>
            {/* Card content */}
          </Card>
        </Box>
      </div>
    );
  };

  return (
    <Box sx={{ height: 600, width: '100%' }}>
      <AutoSizer>
        {({ height, width }) => {
          const columnCount = getColumnCount(width);
          const rowCount = Math.ceil(products.length / columnCount);
          
          return (
            <VirtualGrid
              columnCount={columnCount}
              columnWidth={width / columnCount}
              height={height}
              rowCount={rowCount}
              rowHeight={300} // Adjust based on your card height
              width={width}
              itemData={{ products, columnCount }}
            >
              {Cell}
            </VirtualGrid>
          );
        }}
      </AutoSizer>
    </Box>
  );
};

This implementation uses react-window for virtualization, rendering only the items currently in view, which dramatically improves performance for large lists.

Issue 4: Grid Items Not Wrapping Correctly

Problem: Grid items don't wrap as expected on smaller screens.

Solution: Make sure you're specifying the correct breakpoint props and not overriding the flexbox wrapping behavior:

<Grid 
  container 
  spacing={2}
  sx={{ flexWrap: 'wrap' }} // Ensure wrapping is enabled
>
  {products.map((product) => (
    <Grid 
      xs={12} 
      sm={6} 
      md={4} 
      key={product.id}
      sx={{ minWidth: 0 }} // Ensure items can shrink below their content size
    >
      {/* Product card */}
    </Grid>
  ))}
</Grid>

Complete Product Grid Implementation

Now, let's put everything together to create a complete, production-ready product grid component:

import React, { useState, useMemo, memo } from 'react';
import { Grid } from '@mui/material-next/Grid';
import { 
  Card, CardContent, CardMedia, Typography, Box, 
  FormControl, InputLabel, Select, MenuItem, 
  TextField, CircularProgress, Alert, Button,
  Pagination, Chip, Rating, IconButton, Skeleton
} from '@mui/material';
import FavoriteIcon from '@mui/icons-material/Favorite';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';

// Memoized product card component for better performance
const ProductCard = memo(({ 
  product, 
  onAddToCart, 
  onToggleFavorite, 
  isFavorite 
}) => {
  return (
    <Card
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        transition: 'transform 0.3s ease, box-shadow 0.3s ease',
        '&:hover': {
          transform: 'translateY(-4px)',
          boxShadow: 6
        },
        '&:focus-within': {
          outline: '2px solid',
          outlineColor: 'primary.main',
        }
      }}
    >
      <Box sx={{ position: 'relative' }}>
        <CardMedia
          component="img"
          height="200"
          image={product.image}
          alt={product.name}
          sx={{ objectFit: 'cover' }}
        />
        <IconButton
          aria-label={isFavorite ? `Remove ${product.name} from favorites` : `Add ${product.name} to favorites`}
          sx={{
            position: 'absolute',
            top: 8,
            right: 8,
            bgcolor: 'background.paper',
            '&:hover': { bgcolor: 'background.paper' }
          }}
          onClick={() => onToggleFavorite(product.id)}
        >
          {isFavorite ? <FavoriteIcon color="error" /> : <FavoriteBorderIcon />}
        </IconButton>
        {product.discount > 0 && (
          <Chip
            label={`${product.discount}% OFF`}
            color="error"
            size="small"
            sx={{
              position: 'absolute',
              top: 8,
              left: 8,
            }}
          />
        )}
      </Box>
      <CardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
        <Box sx={{ mb: 1 }}>
          <Rating value={product.rating} precision={0.5} readOnly size="small" />
          <Typography variant="body2" color="text.secondary">
            ({product.reviewCount} reviews)
          </Typography>
        </Box>
        
        <Typography gutterBottom variant="h5" component="h2">
          {product.name}
        </Typography>
        
        <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
          {product.description}
        </Typography>
        
        <Box sx={{ mt: 'auto', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <Box>
            {product.discount > 0 ? (
              <>
                <Typography variant="h6" color="primary" component="span">
                  ${((product.price * (100 - product.discount)) / 100).toFixed(2)}
                </Typography>
                <Typography 
                  variant="body2" 
                  color="text.secondary" 
                  component="span" 
                  sx={{ ml: 1, textDecoration: 'line-through' }}
                >
                  ${product.price.toFixed(2)}
                </Typography>
              </>
            ) : (
              <Typography variant="h6" color="primary">
                ${product.price.toFixed(2)}
              </Typography>
            )}
          </Box>
          <Button 
            variant="contained" 
            size="small"
            startIcon={<ShoppingCartIcon />}
            onClick={() => onAddToCart(product)}
            aria-label={`Add ${product.name} to cart`}
          >
            Add
          </Button>
        </Box>
      </CardContent>
    </Card>
  );
});

// Loading skeleton for product cards
const ProductCardSkeleton = () => (
  <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
    <Skeleton variant="rectangular" height={200} />
    <CardContent sx={{ flexGrow: 1 }}>
      <Skeleton variant="text" width="40%" height={20} sx={{ mb: 1 }} />
      <Skeleton variant="text" width="70%" height={28} sx={{ mb: 1 }} />
      <Skeleton variant="text" width="100%" height={20} />
      <Skeleton variant="text" width="100%" height={20} />
      <Box sx={{ mt: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Skeleton variant="text" width="30%" height={32} />
        <Skeleton variant="rectangular" width="25%" height={36} />
      </Box>
    </CardContent>
  </Card>
);

const ProductGrid = ({ 
  products: initialProducts = [], 
  loading = false, 
  error = null,
  itemsPerPage = 12
}) => {
  const [currentPage, setCurrentPage] = useState(1);
  const [sortBy, setSortBy] = useState('default');
  const [filterText, setFilterText] = useState('');
  const [categoryFilter, setCategoryFilter] = useState('all');
  const [favorites, setFavorites] = useState([]);
  const [cart, setCart] = useState([]);
  
  // Extract unique categories from products
  const categories = useMemo(() => {
    if (!initialProducts.length) return ['all'];
    
    const uniqueCategories = new Set(initialProducts.map(p => p.category));
    return ['all', ...Array.from(uniqueCategories)];
  }, [initialProducts]);
  
  // Handle sorting change
  const handleSort = (event) => {
    setSortBy(event.target.value);
    setCurrentPage(1); // Reset to first page when sorting changes
  };
  
  // Handle filter text change
  const handleFilterChange = (event) => {
    setFilterText(event.target.value);
    setCurrentPage(1); // Reset to first page when filter changes
  };
  
  // Handle category filter change
  const handleCategoryChange = (event) => {
    setCategoryFilter(event.target.value);
    setCurrentPage(1); // Reset to first page when category changes
  };
  
  // Handle page change
  const handlePageChange = (event, value) => {
    setCurrentPage(value);
    // Scroll to top of grid when page changes
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };
  
  // Add to cart handler
  const handleAddToCart = (product) => {
    setCart(prevCart => {
      const existingItem = prevCart.find(item => item.id === product.id);
      
      if (existingItem) {
        return prevCart.map(item => 
          item.id === product.id 
            ? { ...item, quantity: item.quantity + 1 } 
            : item
        );
      } else {
        return [...prevCart, { ...product, quantity: 1 }];
      }
    });
  };
  
  // Toggle favorite handler
  const handleToggleFavorite = (productId) => {
    setFavorites(prevFavorites => {
      if (prevFavorites.includes(productId)) {
        return prevFavorites.filter(id => id !== productId);
      } else {
        return [...prevFavorites, productId];
      }
    });
  };
  
  // Filter and sort products
  const filteredAndSortedProducts = useMemo(() => {
    // First filter the products
    let result = [...initialProducts];
    
    // Apply category filter
    if (categoryFilter !== 'all') {
      result = result.filter(product => product.category === categoryFilter);
    }
    
    // Apply text filter
    if (filterText) {
      const searchTerm = filterText.toLowerCase();
      result = result.filter(
        product => 
          product.name.toLowerCase().includes(searchTerm) || 
          product.description.toLowerCase().includes(searchTerm)
      );
    }
    
    // Then sort them
    switch (sortBy) {
      case 'priceLow':
        return result.sort((a, b) => a.price - b.price);
      case 'priceHigh':
        return result.sort((a, b) => b.price - a.price);
      case 'name':
        return result.sort((a, b) => a.name.localeCompare(b.name));
      case 'rating':
        return result.sort((a, b) => b.rating - a.rating);
      case 'newest':
        return result.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
      default:
        return result;
    }
  }, [initialProducts, filterText, sortBy, categoryFilter]);
  
  // Paginate products
  const paginatedProducts = useMemo(() => {
    const startIndex = (currentPage - 1) * itemsPerPage;
    return filteredAndSortedProducts.slice(startIndex, startIndex + itemsPerPage);
  }, [filteredAndSortedProducts, currentPage, itemsPerPage]);
  
  // Calculate total pages
  const totalPages = Math.ceil(filteredAndSortedProducts.length / itemsPerPage);
  
  return (
    <Box sx={{ maxWidth: 1200, mx: 'auto', p: { xs: 1, sm: 2 } }}>
      {/* Filter and sort controls */}
      <Grid container spacing={2} sx={{ mb: 3 }}>
        <Grid xs={12} sm={6} md={3}>
          <TextField
            fullWidth
            label="Search products"
            variant="outlined"
            value={filterText}
            onChange={handleFilterChange}
            disabled={loading}
            InputProps={{
              sx: { borderRadius: 2 }
            }}
          />
        </Grid>
        <Grid xs={12} sm={6} md={3}>
          <FormControl fullWidth>
            <InputLabel id="category-select-label">Category</InputLabel>
            <Select
              labelId="category-select-label"
              value={categoryFilter}
              label="Category"
              onChange={handleCategoryChange}
              disabled={loading}
              sx={{ borderRadius: 2 }}
            >
              {categories.map(category => (
                <MenuItem key={category} value={category}>
                  {category === 'all' ? 'All Categories' : category}
                </MenuItem>
              ))}
            </Select>
          </FormControl>
        </Grid>
        <Grid xs={12} sm={6} md={3}>
          <FormControl fullWidth>
            <InputLabel id="sort-select-label">Sort by</InputLabel>
            <Select
              labelId="sort-select-label"
              value={sortBy}
              label="Sort by"
              onChange={handleSort}
              disabled={loading}
              sx={{ borderRadius: 2 }}
            >
              <MenuItem value="default">Default</MenuItem>
              <MenuItem value="priceLow">Price: Low to High</MenuItem>
              <MenuItem value="priceHigh">Price: High to Low</MenuItem>
              <MenuItem value="name">Name</MenuItem>
              <MenuItem value="rating">Rating</MenuItem>
              <MenuItem value="newest">Newest</MenuItem>
            </Select>
          </FormControl>
        </Grid>
        <Grid xs={12} sm={6} md={3} sx={{ display: 'flex', alignItems: 'center' }}>
          <Typography variant="body2">
            Showing {filteredAndSortedProducts.length} products
          </Typography>
        </Grid>
      </Grid>
      
      {/* Error message */}
      {error && (
        <Alert severity="error" sx={{ mb: 2 }}>
          {error}
        </Alert>
      )}
      
      {/* Product grid */}
      {loading ? (
        <Grid 
          container 
          spacing={{ xs: 1, sm: 2, md: 3 }}
          columns={{ xs: 1, sm: 2, md: 3, lg: 4 }}
        >
          {Array.from(new Array(itemsPerPage)).map((_, index) => (
            <Grid xs={1} key={index}>
              <ProductCardSkeleton />
            </Grid>
          ))}
        </Grid>
      ) : filteredAndSortedProducts.length === 0 ? (
        <Alert severity="info" sx={{ mb: 2 }}>
          No products found matching your criteria.
        </Alert>
      ) : (
        <>
          <Grid 
            container 
            spacing={{ xs: 1, sm: 2, md: 3 }}
            columns={{ xs: 1, sm: 2, md: 3, lg: 4 }}
          >
            {paginatedProducts.map((product) => (
              <Grid xs={1} key={product.id}>
                <ProductCard 
                  product={product}
                  onAddToCart={handleAddToCart}
                  onToggleFavorite={handleToggleFavorite}
                  isFavorite={favorites.includes(product.id)}
                />
              </Grid>
            ))}
          </Grid>
          
          {/* Pagination */}
          {totalPages > 1 && (
            <Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
              <Pagination 
                count={totalPages} 
                page={currentPage} 
                onChange={handlePageChange}
                color="primary"
                size="large"
                showFirstButton
                showLastButton
              />
            </Box>
          )}
        </>
      )}
    </Box>
  );
};

export default ProductGrid;

This comprehensive implementation includes:

  1. Advanced filtering, sorting, and pagination
  2. Loading states with skeleton placeholders
  3. Favorites and cart functionality
  4. Responsive layout with different column counts based on screen size
  5. Accessibility improvements
  6. Performance optimizations with memoization
  7. Discount display and price formatting
  8. Rating display
  9. Comprehensive error handling

Wrapping Up

In this guide, we've explored MUI Grid v2 in depth and built a production-ready, responsive product grid layout. We've covered the core concepts, advanced features, best practices, and common issues you might encounter when working with Grid v2.

The key takeaways from this guide are:

  1. Grid v2 is a powerful layout system based on CSS Grid that offers more flexibility than the original Grid component
  2. It provides built-in responsive capabilities through breakpoint props
  3. Creating responsive product grids requires thoughtful planning of column counts, spacing, and item layouts
  4. Performance optimizations are crucial for large product lists
  5. Accessibility should be a priority in your grid implementations

With the knowledge and code examples provided in this guide, you should now be well-equipped to create beautiful, responsive product grid layouts using MUI Grid v2. Remember that the best layouts are those that prioritize user experience across all devices while maintaining good performance and accessibility.