--- name: advanced-components description: Advanced MUI component patterns — Autocomplete (async, virtualized, grouped, createFilterOptions), Stepper (non-linear, vertical, mobile), Popover, Popper, Portal, ClickAwayListener, and complex composition triggers: - Autocomplete advanced - Stepper - Popover - Popper - Portal - ClickAwayListener - createFilterOptions - combo box - free solo - async autocomplete - grouped options - non-linear stepper - vertical stepper - mobile stepper allowed-tools: - Read - Glob - Grep - Write - Edit globs: - "*.tsx" - "*.ts" - "*.jsx" --- # Advanced MUI Component Patterns Deep patterns for complex MUI components that go beyond basic usage. --- ## Autocomplete — Advanced Patterns ### Async / Server-Side Options ```tsx import Autocomplete from '@mui/material/Autocomplete'; import TextField from '@mui/material/TextField'; import CircularProgress from '@mui/material/CircularProgress'; import { useState, useEffect, useRef } from 'react'; interface User { id: number; name: string; email: string; } function AsyncAutocomplete() { const [open, setOpen] = useState(false); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); const [inputValue, setInputValue] = useState(''); const debounceRef = useRef(); useEffect(() => { if (!open) return; if (!inputValue) { setOptions([]); return; } // Debounce API calls clearTimeout(debounceRef.current); debounceRef.current = setTimeout(async () => { setLoading(true); try { const res = await fetch(`/api/users?search=${encodeURIComponent(inputValue)}`); const data: User[] = await res.json(); setOptions(data); } finally { setLoading(false); } }, 300); return () => clearTimeout(debounceRef.current); }, [inputValue, open]); return ( setOpen(true)} onClose={() => setOpen(false)} options={options} loading={loading} getOptionLabel={(option) => option.name} isOptionEqualToValue={(option, value) => option.id === value.id} onInputChange={(_, value) => setInputValue(value)} filterOptions={(x) => x} // disable client-side filtering — server handles it renderInput={(params) => ( {loading && } {params.InputProps.endAdornment} ), }, }} /> )} renderOption={(props, option) => (
  • {option.name} {option.email}
  • )} /> ); } ``` ### createFilterOptions — Custom Filtering ```tsx import { createFilterOptions } from '@mui/material/Autocomplete'; interface Film { title: string; year: number; inputValue?: string; // for "create" option } // Custom filter: match start of title, limit to 10 const filter = createFilterOptions({ matchFrom: 'start', // 'start' | 'any' (default: 'any') limit: 10, // max options shown stringify: (option) => option.title, // what to search against ignoreAccents: true, // normalize accents ignoreCase: true, // case-insensitive (default: true) trim: true, // trim whitespace }); // "Create" option pattern — add user-typed value as new option { const filtered = filter(options, params); const { inputValue } = params; // Suggest creating a new value const isExisting = options.some((option) => inputValue === option.title); if (inputValue !== '' && !isExisting) { filtered.push({ inputValue, title: `Add "${inputValue}"`, year: new Date().getFullYear(), }); } return filtered; }} getOptionLabel={(option) => { if (typeof option === 'string') return option; if (option.inputValue) return option.inputValue; return option.title; }} onChange={(_, newValue) => { if (newValue && typeof newValue !== 'string' && newValue.inputValue) { // User chose "Add ..." — create the new item handleCreate(newValue.inputValue); } }} renderInput={(params) => } /> ``` ### Virtualized Autocomplete (10,000+ options) ```tsx import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { forwardRef, HTMLAttributes } from 'react'; const ITEM_HEIGHT = 36; const LISTBOX_PADDING = 8; function renderRow(props: ListChildComponentProps) { const { data, index, style } = props; const dataSet = data[index]; const inlineStyle = { ...style, top: (style.top as number) + LISTBOX_PADDING, }; return (
  • {dataSet[1].label}
  • ); } const ListboxComponent = forwardRef>( function ListboxComponent(props, ref) { const { children, ...other } = props; const items = children as [HTMLAttributes, { label: string; id: string }][]; const itemCount = items.length; const height = Math.min(8, itemCount) * ITEM_HEIGHT + 2 * LISTBOX_PADDING; return (
    {renderRow}
    ); } ); } /> ``` ### Grouped Options ```tsx -b.continent.localeCompare(a.continent))} groupBy={(option) => option.continent} getOptionLabel={(option) => option.name} renderGroup={(params) => (
  • {params.group} {params.children}
  • )} renderInput={(params) => } /> ``` ### Multiple with Chip Limit ```tsx option.label} renderTags={(value, getTagProps, ownerState) => value.map((option, index) => { const { key, ...tagProps } = getTagProps({ index }); return ( ); }) } renderInput={(params) => } /> ``` ### Highlight Matching Text ```tsx import parse from 'autosuggest-highlight/parse'; import match from 'autosuggest-highlight/match'; { const matches = match(option.label, inputValue, { insideWords: true }); const parts = parse(option.label, matches); return (
  • {parts.map((part, index) => ( {part.text} ))}
  • ); }} renderInput={(params) => } /> ``` --- ## Stepper — Advanced Patterns ### Horizontal Stepper with Validation ```tsx import Stepper from '@mui/material/Stepper'; import Step from '@mui/material/Step'; import StepLabel from '@mui/material/StepLabel'; import StepContent from '@mui/material/StepContent'; import Button from '@mui/material/Button'; const steps = ['Account', 'Profile', 'Confirm']; function HorizontalStepper() { const [activeStep, setActiveStep] = useState(0); const [errors, setErrors] = useState>({}); const validateStep = (step: number): boolean => { // Per-step validation logic switch (step) { case 0: return !!formData.email && !!formData.password; case 1: return !!formData.name; default: return true; } }; const handleNext = () => { if (validateStep(activeStep)) { setErrors((prev) => ({ ...prev, [activeStep]: '' })); setActiveStep((prev) => prev + 1); } else { setErrors((prev) => ({ ...prev, [activeStep]: 'Please fill all fields' })); } }; return ( <> {steps.map((label, index) => ( {errors[index]} ) : undefined} > {label} ))} {/* Step content */} {activeStep === 0 && } {activeStep === 1 && } {activeStep === 2 && } ); } ``` ### Vertical Stepper (Step Content) ```tsx {steps.map((step, index) => ( Last step ) : undefined} > {step.label} {step.description} {step.content} ))} ``` ### Non-Linear Stepper ```tsx function NonLinearStepper() { const [activeStep, setActiveStep] = useState(0); const [completed, setCompleted] = useState>({}); const handleStep = (step: number) => () => { setActiveStep(step); // Jump to any step }; const handleComplete = () => { setCompleted((prev) => ({ ...prev, [activeStep]: true })); // Move to next incomplete step const next = steps.findIndex((_, i) => !completed[i] && i !== activeStep); if (next !== -1) setActiveStep(next); }; const allCompleted = Object.keys(completed).length === steps.length; return ( {steps.map((label, index) => ( {label} ))} ); } ``` ### Custom Step Icons & Connectors ```tsx import StepConnector, { stepConnectorClasses } from '@mui/material/StepConnector'; import { StepIconProps } from '@mui/material/StepIcon'; import { styled } from '@mui/material/styles'; import Check from '@mui/icons-material/Check'; const QontoConnector = styled(StepConnector)(({ theme }) => ({ [`&.${stepConnectorClasses.alternativeLabel}`]: { top: 10, left: 'calc(-50% + 16px)', right: 'calc(50% + 16px)', }, [`&.${stepConnectorClasses.active}`]: { [`& .${stepConnectorClasses.line}`]: { borderColor: theme.palette.primary.main, }, }, [`&.${stepConnectorClasses.completed}`]: { [`& .${stepConnectorClasses.line}`]: { borderColor: theme.palette.primary.main, }, }, [`& .${stepConnectorClasses.line}`]: { borderColor: theme.palette.divider, borderTopWidth: 3, borderRadius: 1, }, })); function QontoStepIcon(props: StepIconProps) { const { active, completed, className } = props; return ( {completed ? ( ) : ( )} ); } }> {steps.map((label) => ( {label} ))} ``` ### MobileStepper (Dots / Progress / Text) ```tsx import MobileStepper from '@mui/material/MobileStepper'; import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'; import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; Next } backButton={ } /> ``` --- ## Popover ```tsx import Popover from '@mui/material/Popover'; // Anchor to element const [anchorEl, setAnchorEl] = useState(null); setAnchorEl(null)} anchorOrigin={{ vertical: 'bottom', // 'top' | 'center' | 'bottom' | number horizontal: 'left', // 'left' | 'center' | 'right' | number }} transformOrigin={{ vertical: 'top', horizontal: 'left', }} slotProps={{ paper: { sx: { p: 2, maxWidth: 300 }, }, }} > Popover content // Mouse-follow popover Follows cursor // Virtual element (e.g., text selection) ({ top: selectionRect.top, left: selectionRect.left, bottom: selectionRect.bottom, right: selectionRect.right, width: selectionRect.width, height: selectionRect.height, x: selectionRect.x, y: selectionRect.y, toJSON: () => {}, }), }} > ``` --- ## Popper Lower-level than Popover — no backdrop, no click-away handling built in. ```tsx import Popper from '@mui/material/Popper'; import ClickAwayListener from '@mui/material/ClickAwayListener'; import Fade from '@mui/material/Fade'; import Paper from '@mui/material/Paper'; const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); {({ TransitionProps }) => ( setAnchorEl(null)}> Popper content )} ``` ### Popper Placements ``` top-start top top-end left-start right-start left right left-end right-end bottom-start bottom bottom-end ``` --- ## Portal Renders children into a different part of the DOM tree. ```tsx import Portal from '@mui/material/Portal'; // Render into document.body (default) Floating element // Render into a specific container const containerRef = useRef(null);
    Rendered inside the div above // Disable portal (render in place) Rendered in the normal tree position ``` --- ## ClickAwayListener Detects clicks outside the wrapped element. ```tsx import ClickAwayListener from '@mui/material/ClickAwayListener'; {open && ( Item 1 Item 2 )} ``` **Gotcha**: ClickAwayListener only works with a single child element. If wrapping multiple elements, wrap them in a `
    ` or ``. --- ## NoSsr Defers rendering to client only — useful for client-dependent content. ```tsx import NoSsr from '@mui/material/NoSsr'; // Content only renders on client (prevents SSR hydration mismatch) }> {/* Uses window.navigator, fails on server */} // Defer to second frame (avoid blocking first paint) ``` --- ## Composition Patterns ### Component Prop (Polymorphic) Many MUI components accept a `component` prop to change the rendered HTML element: ```tsx // Button as a router link import { Link as RouterLink } from 'react-router-dom'; // ListItemButton as a router link // Card as an article Article content // Typography as a label Email ``` ### Forwarding sx to Inner Components ```tsx interface CustomCardProps { title: string; children: React.ReactNode; sx?: SxProps; } function CustomCard({ title, children, sx }: CustomCardProps) { return ( {title} {children} ); } // Usage — sx is properly merged Content ``` ### Render Props with MUI (Headless Patterns) ```tsx import { useAutocomplete } from '@mui/material/useAutocomplete'; function CustomCombobox({ options, label }: { options: string[]; label: string }) { const { getRootProps, getInputProps, getListboxProps, getOptionProps, groupedOptions, focused, value, } = useAutocomplete({ options, getOptionLabel: (option) => option, }); return (
    {groupedOptions.length > 0 && (
      {groupedOptions.map((option, index) => (
    • {option}
    • ))}
    )}
    ); } ```