Building Reusable Form Actions with MUI Button: A Complete Guide to Theme Overrides and Patterns
As a front-end developer, I've spent countless hours refactoring the same button components across different projects. One pattern I've found particularly valuable is creating reusable form action buttons with Material UI. In this guide, I'll show you how to leverage MUI Button's flexibility to create a reusable pattern for form actions that's both maintainable and themeable.
By the end of this article, you'll have a robust, reusable button system for your forms that will save you time and ensure consistency across your application.
What You'll Learn
In this comprehensive guide, you'll learn:
- How to analyze MUI Button's API to build extensible components
- Creating a reusable FormActionButton component with proper prop handling
- Implementing theme overrides for consistent styling across your application
- Building button variants for different form actions (submit, cancel, reset)
- Handling loading states, disabled states, and accessibility
- Integrating with form libraries like Formik and React Hook Form
- Advanced patterns for conditional rendering and composition
Understanding MUI Button: A Deep Dive
Before we start building our reusable pattern, let's understand the MUI Button component thoroughly. This will help us make informed decisions about our implementation.
Button Variants and Properties
MUI Button comes with three main variants: contained
, outlined
, and text
. Each variant has its own visual style and is suitable for different situations in your UI.
// Contained buttons (high emphasis)
<Button variant="contained">Submit</Button>
// Outlined buttons (medium emphasis)
<Button variant="outlined">Cancel</Button>
// Text buttons (low emphasis)
<Button variant="text">Reset</Button>
The Button component accepts numerous props that control its appearance and behavior. Here are the most important ones:
Prop | Type | Default | Description |
---|---|---|---|
children | node | - | The content of the button |
color | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning' | string | 'primary' | The color of the button |
disabled | boolean | false | If true, the button will be disabled |
disableElevation | boolean | false | If true, no elevation is used |
endIcon | node | - | Element placed after the children |
fullWidth | boolean | false | If true, the button will take up the full width of its container |
href | string | - | The URL to link to when the button is clicked |
size | 'small' | 'medium' | 'large' | 'medium' | The size of the button |
startIcon | node | - | Element placed before the children |
type | 'button' | 'submit' | 'reset' | 'button' | The type of button |
variant | 'contained' | 'outlined' | 'text' | 'text' | The variant to use |
Styling and Customization Options
MUI Button can be styled in multiple ways:
- Using the
sx
prop: For one-off styling needs - Theme customization: For app-wide button styling
- Styled API: For creating styled components based on Button
Here's a quick example of using the sx
prop:
<Button
variant="contained"
sx={{
backgroundColor: 'darkblue',
'&:hover': {
backgroundColor: 'navy',
},
borderRadius: 2,
textTransform: 'none',
}}
>
Custom Button
</Button>
For theme customization, you'd modify your theme like this:
const theme = createTheme({
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
borderRadius: 8,
},
contained: {
boxShadow: 'none',
'&:hover': {
boxShadow: 'none',
},
},
},
},
},
});
Accessibility Considerations
MUI Button is designed with accessibility in mind, but there are still important considerations:
- Always provide meaningful text for screen readers
- Use appropriate color contrast
- Ensure keyboard navigation works correctly
- Use proper ARIA attributes when necessary
For example, if your button only contains an icon, you should provide an aria-label
:
<Button
aria-label="Add to cart"
startIcon={<ShoppingCartIcon />}
>
Add to cart
</Button>
Building a Reusable Form Action Button
Now that we understand the MUI Button component, let's create our reusable FormActionButton component. We'll start with a basic implementation and then enhance it.
Step 1: Create the Base Component
First, let's create a base component that extends the MUI Button with form-specific functionality.
import React from 'react';
import { Button, CircularProgress } from '@mui/material';
import PropTypes from 'prop-types';
const FormActionButton = ({
children,
loading = false,
actionType = 'submit',
variant = 'contained',
color = 'primary',
fullWidth = false,
disabled = false,
onClick,
...props
}) => {
// Map actionType to button type and variant
const buttonTypes = {
submit: { type: 'submit', defaultVariant: 'contained', defaultColor: 'primary' },
reset: { type: 'reset', defaultVariant: 'outlined', defaultColor: 'secondary' },
cancel: { type: 'button', defaultVariant: 'text', defaultColor: 'inherit' },
};
const { type, defaultVariant, defaultColor } = buttonTypes[actionType] || buttonTypes.submit;
// Use provided variant/color or default based on actionType
const buttonVariant = variant || defaultVariant;
const buttonColor = color || defaultColor;
return (
<Button
type={type}
variant={buttonVariant}
color={buttonColor}
fullWidth={fullWidth}
disabled={disabled || loading}
onClick={onClick}
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : null}
{...props}
>
{children}
</Button>
);
};
FormActionButton.propTypes = {
children: PropTypes.node.isRequired,
loading: PropTypes.bool,
actionType: PropTypes.oneOf(['submit', 'reset', 'cancel']),
variant: PropTypes.oneOf(['contained', 'outlined', 'text']),
color: PropTypes.string,
fullWidth: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func,
};
export default FormActionButton;
This base component provides several advantages:
- Action Type Mapping: We map
actionType
to appropriate button types and default styling - Loading State: We handle loading states with a spinner
- Sensible Defaults: We provide reasonable defaults based on the action type
- Prop Forwarding: We forward all other props to the underlying Button component
Step 2: Create Specialized Button Components
Now, let's create specialized buttons for common form actions. This will make our form code more readable and consistent.
import React from 'react';
import FormActionButton from './FormActionButton';
export const SubmitButton = (props) => (
<FormActionButton actionType="submit" {...props} />
);
export const ResetButton = (props) => (
<FormActionButton actionType="reset" {...props} />
);
export const CancelButton = (props) => (
<FormActionButton actionType="cancel" {...props} />
);
Step 3: Add Theme Customization
To ensure consistent styling across our application, let's add theme customization for our form action buttons.
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
components: {
MuiButton: {
styleOverrides: {
// Base styles for all buttons
root: {
textTransform: 'none',
borderRadius: 8,
padding: '8px 24px',
fontWeight: 600,
},
// Styles for contained buttons
contained: {
boxShadow: 'none',
'&:hover': {
boxShadow: 'none',
},
},
// Styles for outlined buttons
outlined: {
borderWidth: 2,
'&:hover': {
borderWidth: 2,
},
},
},
// Default props for all buttons
defaultProps: {
disableElevation: true,
},
// Variant-specific styles
variants: [
{
props: { variant: 'contained', color: 'primary' },
style: {
backgroundColor: '#1976d2',
'&:hover': {
backgroundColor: '#1565c0',
},
},
},
{
props: { variant: 'contained', color: 'secondary' },
style: {
backgroundColor: '#9c27b0',
'&:hover': {
backgroundColor: '#7b1fa2',
},
},
},
{
props: { actionType: 'submit' },
style: {
minWidth: 120,
},
},
{
props: { actionType: 'cancel' },
style: {
color: '#666',
},
},
],
},
},
});
export default theme;
Step 4: Create a Custom Theme Provider for Form Actions
To make our theme customization more focused, let's create a dedicated theme provider for form actions.
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
const FormActionThemeProvider = ({ children }) => {
// Get the parent theme
const parentTheme = useTheme();
// Create a new theme that extends the parent theme
const formActionTheme = createTheme({
...parentTheme,
components: {
...parentTheme.components,
MuiButton: {
...parentTheme.components?.MuiButton,
styleOverrides: {
...parentTheme.components?.MuiButton?.styleOverrides,
// Form action specific overrides
root: {
...parentTheme.components?.MuiButton?.styleOverrides?.root,
fontSize: '0.9rem',
transition: 'all 0.2s ease-in-out',
},
},
variants: [
...(parentTheme.components?.MuiButton?.variants || []),
{
props: { actionType: 'submit' },
style: {
minWidth: 120,
fontWeight: 600,
},
},
{
props: { actionType: 'reset' },
style: {
minWidth: 100,
},
},
{
props: { actionType: 'cancel' },
style: {
minWidth: 100,
color: parentTheme.palette.text.secondary,
},
},
],
},
},
});
return <ThemeProvider theme={formActionTheme}>{children}</ThemeProvider>;
};
export default FormActionThemeProvider;
Step-by-Step Implementation Guide
Now let's walk through a complete implementation of our form action button pattern in a real-world application.
Step 1: Set Up Your Project
First, make sure you have the necessary dependencies installed:
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled prop-types
Step 2: Create the Theme Configuration
Create a file called theme.js
with your theme configuration:
// src/theme.js
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
primary: {
main: '#2196f3',
light: '#64b5f6',
dark: '#1976d2',
contrastText: '#fff',
},
secondary: {
main: '#f50057',
light: '#ff4081',
dark: '#c51162',
contrastText: '#fff',
},
success: {
main: '#4caf50',
light: '#81c784',
dark: '#388e3c',
contrastText: '#fff',
},
error: {
main: '#f44336',
light: '#e57373',
dark: '#d32f2f',
contrastText: '#fff',
},
},
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
button: {
textTransform: 'none',
fontWeight: 500,
},
},
components: {
MuiButton: {
styleOverrides: {
root: {
borderRadius: 8,
padding: '8px 16px',
},
contained: {
boxShadow: 'none',
'&:hover': {
boxShadow: 'none',
},
},
},
defaultProps: {
disableElevation: true,
},
},
},
});
export default theme;
Step 3: Create the FormActionButton Component
Create a file called FormActionButton.js
:
// src/components/FormActionButton.js
import React from 'react';
import { Button, CircularProgress } from '@mui/material';
import PropTypes from 'prop-types';
const FormActionButton = ({
children,
loading = false,
actionType = 'submit',
variant,
color,
size = 'medium',
fullWidth = false,
disabled = false,
onClick,
startIcon,
endIcon,
sx = {},
...props
}) => {
// Map actionType to button type and default styling
const actionTypeConfig = {
submit: {
type: 'submit',
defaultVariant: 'contained',
defaultColor: 'primary',
loadingText: 'Submitting...',
},
reset: {
type: 'reset',
defaultVariant: 'outlined',
defaultColor: 'secondary',
loadingText: 'Resetting...',
},
cancel: {
type: 'button',
defaultVariant: 'text',
defaultColor: 'inherit',
loadingText: 'Cancelling...',
},
delete: {
type: 'button',
defaultVariant: 'contained',
defaultColor: 'error',
loadingText: 'Deleting...',
},
};
const config = actionTypeConfig[actionType] || actionTypeConfig.submit;
// Use provided variant/color or default based on actionType
const buttonType = config.type;
const buttonVariant = variant || config.defaultVariant;
const buttonColor = color || config.defaultColor;
// Handle loading state
const buttonText = loading && props.loadingText ? props.loadingText : children;
const buttonStartIcon = loading ? (
<CircularProgress size={20} color="inherit" />
) : (
startIcon
);
// Merge sx props
const defaultSx = {
position: 'relative',
...(actionType === 'submit' && { minWidth: 120 }),
...(actionType === 'cancel' && { minWidth: 100 }),
};
return (
<Button
type={buttonType}
variant={buttonVariant}
color={buttonColor}
size={size}
fullWidth={fullWidth}
disabled={disabled || loading}
onClick={onClick}
startIcon={buttonStartIcon}
endIcon={loading ? null : endIcon}
sx={{ ...defaultSx, ...sx }}
{...props}
data-action-type={actionType}
>
{buttonText}
</Button>
);
};
FormActionButton.propTypes = {
children: PropTypes.node.isRequired,
loading: PropTypes.bool,
loadingText: PropTypes.string,
actionType: PropTypes.oneOf(['submit', 'reset', 'cancel', 'delete']),
variant: PropTypes.oneOf(['contained', 'outlined', 'text']),
color: PropTypes.string,
size: PropTypes.oneOf(['small', 'medium', 'large']),
fullWidth: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func,
startIcon: PropTypes.node,
endIcon: PropTypes.node,
sx: PropTypes.object,
};
export default FormActionButton;
Step 4: Create Specialized Button Components
Create a file called FormButtons.js
:
// src/components/FormButtons.js
import React from 'react';
import FormActionButton from './FormActionButton';
import PropTypes from 'prop-types';
import {
Save as SaveIcon,
Cancel as CancelIcon,
Refresh as RefreshIcon,
Delete as DeleteIcon
} from '@mui/icons-material';
export const SubmitButton = ({ children = 'Submit', startIcon = <SaveIcon />, ...props }) => (
<FormActionButton
actionType="submit"
startIcon={startIcon}
{...props}
>
{children}
</FormActionButton>
);
export const ResetButton = ({ children = 'Reset', startIcon = <RefreshIcon />, ...props }) => (
<FormActionButton
actionType="reset"
startIcon={startIcon}
{...props}
>
{children}
</FormActionButton>
);
export const CancelButton = ({ children = 'Cancel', startIcon = <CancelIcon />, ...props }) => (
<FormActionButton
actionType="cancel"
startIcon={startIcon}
{...props}
>
{children}
</FormActionButton>
);
export const DeleteButton = ({ children = 'Delete', startIcon = <DeleteIcon />, ...props }) => (
<FormActionButton
actionType="delete"
startIcon={startIcon}
{...props}
>
{children}
</FormActionButton>
);
// Common PropTypes for all specialized buttons
const buttonPropTypes = {
children: PropTypes.node,
loading: PropTypes.bool,
loadingText: PropTypes.string,
variant: PropTypes.oneOf(['contained', 'outlined', 'text']),
color: PropTypes.string,
size: PropTypes.oneOf(['small', 'medium', 'large']),
fullWidth: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func,
startIcon: PropTypes.node,
endIcon: PropTypes.node,
sx: PropTypes.object,
};
SubmitButton.propTypes = buttonPropTypes;
ResetButton.propTypes = buttonPropTypes;
CancelButton.propTypes = buttonPropTypes;
DeleteButton.propTypes = buttonPropTypes;
Step 5: Create a Form Actions Container Component
To organize form buttons consistently, let's create a container component:
// src/components/FormActions.js
import React from 'react';
import { Box } from '@mui/material';
import PropTypes from 'prop-types';
const FormActions = ({
children,
spacing = 2,
direction = 'row',
justifyContent = 'flex-end',
sx = {},
...props
}) => {
return (
<Box
sx={{
display: 'flex',
flexDirection: direction,
justifyContent,
gap: spacing,
mt: 3,
...sx,
}}
{...props}
>
{children}
</Box>
);
};
FormActions.propTypes = {
children: PropTypes.node.isRequired,
spacing: PropTypes.number,
direction: PropTypes.oneOf(['row', 'column', 'row-reverse', 'column-reverse']),
justifyContent: PropTypes.string,
sx: PropTypes.object,
};
export default FormActions;
Step 6: Create Theme Overrides for Form Actions
Now, let's create a dedicated theme provider for form actions:
// src/components/FormActionThemeProvider.js
import React from 'react';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
import PropTypes from 'prop-types';
const FormActionThemeProvider = ({ children, customOverrides = {} }) => {
// Get the parent theme
const parentTheme = useTheme();
// Create a new theme that extends the parent theme
const formActionTheme = createTheme({
...parentTheme,
components: {
...parentTheme.components,
MuiButton: {
...parentTheme.components?.MuiButton,
styleOverrides: {
...parentTheme.components?.MuiButton?.styleOverrides,
// Form action specific overrides
root: {
...parentTheme.components?.MuiButton?.styleOverrides?.root,
fontSize: '0.9rem',
transition: 'all 0.2s ease-in-out',
...customOverrides.root,
},
// Variant specific overrides
contained: {
...parentTheme.components?.MuiButton?.styleOverrides?.contained,
...customOverrides.contained,
},
outlined: {
...parentTheme.components?.MuiButton?.styleOverrides?.outlined,
...customOverrides.outlined,
},
text: {
...parentTheme.components?.MuiButton?.styleOverrides?.text,
...customOverrides.text,
},
},
variants: [
...(parentTheme.components?.MuiButton?.variants || []),
// Action type specific styling
{
props: { 'data-action-type': 'submit' },
style: {
minWidth: 120,
fontWeight: 600,
...customOverrides.submit,
},
},
{
props: { 'data-action-type': 'reset' },
style: {
minWidth: 100,
...customOverrides.reset,
},
},
{
props: { 'data-action-type': 'cancel' },
style: {
minWidth: 100,
color: parentTheme.palette.text.secondary,
...customOverrides.cancel,
},
},
{
props: { 'data-action-type': 'delete' },
style: {
minWidth: 120,
...customOverrides.delete,
},
},
],
},
},
});
return <ThemeProvider theme={formActionTheme}>{children}</ThemeProvider>;
};
FormActionThemeProvider.propTypes = {
children: PropTypes.node.isRequired,
customOverrides: PropTypes.object,
};
export default FormActionThemeProvider;
Step 7: Use the Components in a Form
Now, let's put everything together in a form:
// src/components/UserForm.js
import React, { useState } from 'react';
import {
Box,
TextField,
Typography,
Paper,
Divider
} from '@mui/material';
import FormActions from './FormActions';
import { SubmitButton, ResetButton, CancelButton } from './FormButtons';
import FormActionThemeProvider from './FormActionThemeProvider';
const UserForm = ({ onSubmit, onCancel, initialData = {}, isEditMode = false }) => {
const [formData, setFormData] = useState({
firstName: initialData.firstName || '',
lastName: initialData.lastName || '',
email: initialData.email || '',
...initialData
});
const [loading, setLoading] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
if (onSubmit) {
await onSubmit(formData);
}
} catch (error) {
console.error('Form submission error:', error);
} finally {
setLoading(false);
}
};
const handleReset = () => {
setFormData({
firstName: initialData.firstName || '',
lastName: initialData.lastName || '',
email: initialData.email || '',
...initialData
});
};
return (
<Paper
elevation={2}
sx={{
p: 3,
maxWidth: 600,
mx: 'auto',
mt: 4
}}
>
<Typography variant="h5" gutterBottom>
{isEditMode ? 'Edit User' : 'Create New User'}
</Typography>
<Divider sx={{ mb: 3 }} />
<Box component="form" onSubmit={handleSubmit} noValidate>
<TextField
margin="normal"
required
fullWidth
id="firstName"
label="First Name"
name="firstName"
value={formData.firstName}
onChange={handleChange}
autoFocus
/>
<TextField
margin="normal"
required
fullWidth
id="lastName"
label="Last Name"
name="lastName"
value={formData.lastName}
onChange={handleChange}
/>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
<FormActionThemeProvider
customOverrides={{
submit: {
fontWeight: 700,
},
cancel: {
fontStyle: 'italic',
}
}}
>
<FormActions>
<CancelButton
onClick={onCancel}
disabled={loading}
/>
<ResetButton
onClick={handleReset}
disabled={loading}
/>
<SubmitButton
loading={loading}
loadingText="Saving..."
>
{isEditMode ? 'Update User' : 'Create User'}
</SubmitButton>
</FormActions>
</FormActionThemeProvider>
</Box>
</Paper>
);
};
export default UserForm;
Step 8: Use the Form in Your Application
Finally, use the form in your application:
// src/App.js
import React from 'react';
import { ThemeProvider, CssBaseline, Container, Box } from '@mui/material';
import theme from './theme';
import UserForm from './components/UserForm';
function App() {
const handleSubmit = (data) => {
console.log('Form submitted with:', data);
alert('Form submitted successfully!');
};
const handleCancel = () => {
console.log('Form cancelled');
alert('Form cancelled');
};
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container>
<Box sx={{ my: 4 }}>
<UserForm
onSubmit={handleSubmit}
onCancel={handleCancel}
initialData={{
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com'
}}
isEditMode={true}
/>
</Box>
</Container>
</ThemeProvider>
);
}
export default App;
Advanced Form Action Button Patterns
Now that we have a solid foundation, let's explore some advanced patterns for our form action buttons.
Integration with Form Libraries
Our FormActionButton works great with form libraries like Formik and React Hook Form. Here's an example with Formik:
// src/components/FormikUserForm.js
import React from 'react';
import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';
import {
Box,
Typography,
Paper,
Divider
} from '@mui/material';
import { TextField } from 'formik-mui';
import FormActions from './FormActions';
import { SubmitButton, ResetButton, CancelButton } from './FormButtons';
import FormActionThemeProvider from './FormActionThemeProvider';
// Validation schema
const UserSchema = Yup.object().shape({
firstName: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Required'),
lastName: Yup.string()
.min(2, 'Too Short!')
.max(50, 'Too Long!')
.required('Required'),
email: Yup.string()
.email('Invalid email')
.required('Required'),
});
const FormikUserForm = ({ onSubmit, onCancel, initialValues = {}, isEditMode = false }) => {
const defaultValues = {
firstName: '',
lastName: '',
email: '',
...initialValues
};
return (
<Paper elevation={2} sx={{ p: 3, maxWidth: 600, mx: 'auto', mt: 4 }}>
<Typography variant="h5" gutterBottom>
{isEditMode ? 'Edit User' : 'Create New User'}
</Typography>
<Divider sx={{ mb: 3 }} />
<Formik
initialValues={defaultValues}
validationSchema={UserSchema}
onSubmit={async (values, { setSubmitting }) => {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
if (onSubmit) {
await onSubmit(values);
}
setSubmitting(false);
}}
>
{({ isSubmitting, resetForm, dirty, isValid }) => (
<Form>
<Field
component={TextField}
name="firstName"
label="First Name"
fullWidth
margin="normal"
required
/>
<Field
component={TextField}
name="lastName"
label="Last Name"
fullWidth
margin="normal"
required
/>
<Field
component={TextField}
name="email"
type="email"
label="Email Address"
fullWidth
margin="normal"
required
/>
<FormActionThemeProvider>
<FormActions>
<CancelButton
onClick={onCancel}
disabled={isSubmitting}
/>
<ResetButton
onClick={() => resetForm()}
disabled={isSubmitting || !dirty}
/>
<SubmitButton
loading={isSubmitting}
loadingText="Saving..."
disabled={!dirty || !isValid}
>
{isEditMode ? 'Update User' : 'Create User'}
</SubmitButton>
</FormActions>
</FormActionThemeProvider>
</Form>
)}
</Formik>
</Paper>
);
};
export default FormikUserForm;
Conditional Rendering Based on Form State
We can enhance our buttons to respond to form state changes:
// src/components/SmartFormActions.js
import React from 'react';
import { useFormikContext } from 'formik';
import FormActions from './FormActions';
import { SubmitButton, ResetButton, CancelButton } from './FormButtons';
import FormActionThemeProvider from './FormActionThemeProvider';
const SmartFormActions = ({
onCancel,
showReset = true,
showCancel = true,
submitText,
resetText,
cancelText,
direction = 'row',
spacing = 2,
...props
}) => {
const { isSubmitting, dirty, isValid, resetForm } = useFormikContext();
return (
<FormActionThemeProvider>
<FormActions direction={direction} spacing={spacing} {...props}>
{showCancel && (
<CancelButton
onClick={onCancel}
disabled={isSubmitting}
>
{cancelText || 'Cancel'}
</CancelButton>
)}
{showReset && (
<ResetButton
onClick={() => resetForm()}
disabled={isSubmitting || !dirty}
>
{resetText || 'Reset'}
</ResetButton>
)}
<SubmitButton
loading={isSubmitting}
loadingText="Saving..."
disabled={!dirty || !isValid}
>
{submitText || 'Submit'}
</SubmitButton>
</FormActions>
</FormActionThemeProvider>
);
};
export default SmartFormActions;
And use it in our form:
// Usage in a Formik form
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
{/* Form fields */}
<SmartFormActions
onCancel={handleCancel}
submitText={isEditMode ? 'Update User' : 'Create User'}
/>
</Form>
</Formik>
Creating Button Groups for Common Form Patterns
For even more reusability, let's create button groups for common form patterns:
// src/components/FormButtonGroups.js
import React from 'react';
import FormActions from './FormActions';
import { SubmitButton, ResetButton, CancelButton, DeleteButton } from './FormButtons';
import FormActionThemeProvider from './FormActionThemeProvider';
// Standard form buttons: Submit, Reset, Cancel
export const StandardFormButtons = ({
onCancel,
submitText = 'Submit',
resetText = 'Reset',
cancelText = 'Cancel',
isSubmitting = false,
isDirty = true,
isValid = true,
onReset,
...props
}) => (
<FormActionThemeProvider>
<FormActions {...props}>
<CancelButton
onClick={onCancel}
disabled={isSubmitting}
>
{cancelText}
</CancelButton>
<ResetButton
onClick={onReset}
disabled={isSubmitting || !isDirty}
>
{resetText}
</ResetButton>
<SubmitButton
loading={isSubmitting}
loadingText="Saving..."
disabled={!isDirty || !isValid}
>
{submitText}
</SubmitButton>
</FormActions>
</FormActionThemeProvider>
);
// Edit form buttons: Submit, Cancel, Delete
export const EditFormButtons = ({
onCancel,
onDelete,
submitText = 'Save Changes',
cancelText = 'Cancel',
deleteText = 'Delete',
isSubmitting = false,
isDeleting = false,
isDirty = true,
isValid = true,
...props
}) => (
<FormActionThemeProvider>
<FormActions {...props}>
<CancelButton
onClick={onCancel}
disabled={isSubmitting || isDeleting}
>
{cancelText}
</CancelButton>
<DeleteButton
onClick={onDelete}
loading={isDeleting}
loadingText="Deleting..."
disabled={isSubmitting}
>
{deleteText}
</DeleteButton>
<SubmitButton
loading={isSubmitting}
loadingText="Saving..."
disabled={!isDirty || !isValid || isDeleting}
>
{submitText}
</SubmitButton>
</FormActions>
</FormActionThemeProvider>
);
// Simple form buttons: Submit, Cancel
export const SimpleFormButtons = ({
onCancel,
submitText = 'Submit',
cancelText = 'Cancel',
isSubmitting = false,
isDirty = true,
isValid = true,
...props
}) => (
<FormActionThemeProvider>
<FormActions {...props}>
<CancelButton
onClick={onCancel}
disabled={isSubmitting}
>
{cancelText}
</CancelButton>
<SubmitButton
loading={isSubmitting}
loadingText="Saving..."
disabled={!isDirty || !isValid}
>
{submitText}
</SubmitButton>
</FormActions>
</FormActionThemeProvider>
);
Best Practices and Common Issues
Here are some best practices and common issues you might encounter when implementing this pattern:
Best Practices
-
Consistent Button Ordering: Always maintain the same button order across your forms for consistency. A common pattern is Cancel → Reset → Submit (from left to right).
-
Visual Hierarchy: Use visual styling to emphasize the primary action (usually submit) and de-emphasize secondary actions (cancel, reset).
-
Loading States: Always provide visual feedback during asynchronous operations by showing loading indicators.
-
Disable During Submission: Disable all buttons during form submission to prevent multiple submissions.
-
Responsive Design: Ensure your form actions work well on mobile by using responsive layouts:
<FormActions
direction={{ xs: 'column', sm: 'row' }}
sx={{
'& > button': {
width: { xs: '100%', sm: 'auto' }
}
}}
>
{/* Buttons */}
</FormActions>
- Accessibility: Ensure your buttons have proper ARIA attributes and keyboard navigation:
<SubmitButton
aria-label="Submit form"
loading={isSubmitting}
>
Submit
</SubmitButton>
Common Issues and Solutions
-
Issue: Buttons not updating when form state changes. Solution: Make sure your buttons are inside the form component context and have access to the form state.
-
Issue: Theme overrides not being applied. Solution: Ensure your FormActionThemeProvider is properly nested and that the data attributes are correctly set.
-
Issue: Inconsistent button sizes across forms. Solution: Use the
minWidth
property in your theme overrides to ensure consistent button widths. -
Issue: Loading state not visible on small buttons. Solution: Adjust the size of the CircularProgress component and consider using a minimum width for buttons.
-
Issue: Buttons wrapping or overflowing on small screens. Solution: Use responsive direction and width properties for your FormActions component.
Performance Considerations
When implementing form action buttons, consider these performance optimizations:
- Memoize Button Components: Use React.memo to prevent unnecessary re-renders:
const SubmitButton = React.memo(({ children = 'Submit', ...props }) => (
<FormActionButton actionType="submit" {...props}>
{children}
</FormActionButton>
));
- Debounce Submit Handlers: For forms with expensive validation or submission logic, debounce the submit handler:
import { debounce } from 'lodash';
// Inside your component
const debouncedSubmit = React.useMemo(
() => debounce((values) => {
// Submit logic here
console.log('Submitting:', values);
}, 300),
[]
);
- Lazy Load Non-Critical Components: If your form has multiple sections or complex button logic, consider lazy loading:
const DeleteConfirmationDialog = React.lazy(() =>
import('./DeleteConfirmationDialog')
);
// Then in your component
{showDeleteConfirmation && (
<React.Suspense fallback={<CircularProgress />}>
<DeleteConfirmationDialog
onConfirm={handleDelete}
onCancel={() => setShowDeleteConfirmation(false)}
/>
</React.Suspense>
)}
Wrapping Up
In this guide, we've built a comprehensive reusable pattern for form actions using MUI Button. We've covered creating specialized button components, theme customization, integration with form libraries, and advanced patterns for different form scenarios.
By implementing this pattern, you'll achieve:
- Consistent styling and behavior across all your forms
- Reduced boilerplate code for common form actions
- Better maintainability through centralized theme configuration
- Enhanced user experience with proper loading states and visual feedback
- Improved accessibility for all users
The approach we've taken is flexible and can be extended to support additional requirements as your application grows. By centralizing your form action button logic, you'll make it easier to implement global changes to your UI and ensure a consistent experience for your users.