Menu

Building a Validated Signup Form with React MUI TextField, Yup, and React Hook Form

As a front-end developer, creating forms is a common task—but building forms that are both visually appealing and properly validated can be challenging. Material UI's TextField component combined with React Hook Form and Yup validation offers a powerful solution for creating professional, user-friendly forms with robust validation.

In this guide, I'll walk you through creating a complete signup form using MUI's TextField component with proper validation. You'll learn not just the basics, but also advanced techniques for creating forms that are both functional and provide excellent user experience.

Learning Objectives

By the end of this tutorial, you'll know how to:

  • Implement MUI TextField components in various configurations
  • Set up form validation using Yup schema validation
  • Integrate React Hook Form for efficient form state management
  • Create a complete signup form with real-time validation feedback
  • Handle form submission and error states
  • Apply best practices for form accessibility
  • Customize TextField styling to match your design requirements

Understanding MUI TextField Component

The TextField component is one of Material UI's most versatile and frequently used components. It's a complete form control that includes a label, input, and helper text with error messaging capabilities.

Core Props and Features

TextField is a wrapper around several other components, including FormControl, InputLabel, Input or FilledInput or OutlinedInput, and FormHelperText. This composition gives TextField its power and flexibility.

Here's a breakdown of the essential props:

PropTypeDefaultDescription
labelnodeundefinedThe label content
variant'filled' | 'outlined' | 'standard''outlined'The variant to use
errorboolfalseIf true, the input will indicate an error state
helperTextnodeundefinedHelper text to display below the input
size'small' | 'medium''medium'The size of the component
fullWidthboolfalseIf true, the input will take up the full width of its container
requiredboolfalseIf true, the label will indicate that the input is required
disabledboolfalseIf true, the component is disabled
typestring'text'Type of the input element (e.g., 'password', 'email')
multilineboolfalseIf true, a textarea element will be rendered instead of an input
InputPropsobjectProps applied to the Input element

TextField Variants

MUI's TextField comes in three main variants:

  1. Outlined (default) - Has a visible border around the input field
  2. Filled - Has a solid background with the input appearing inset
  3. Standard - Classic look with an underline and no background

Each variant has its own visual style and can be further customized to match your application's design system.


import TextField from '@mui/material/TextField';

// Basic examples of the three variants
export default function TextFieldVariants() {
  return (
    <div>
      <TextField
        label="Outlined"
        variant="outlined"
        margin="normal"
      />
      <TextField
        label="Filled"
        variant="filled"
        margin="normal"
      />
      <TextField
        label="Standard"
        variant="standard"
        margin="normal"
      />
    </div>
  );
}

Controlled vs Uncontrolled Usage

TextField can be used in both controlled and uncontrolled modes:

Controlled - The value is managed by React state:


import { useState } from 'react';
import TextField from '@mui/material/TextField';

function ControlledTextField() {
  const [value, setValue] = useState('');
  
  const handleChange = (event) => {
    setValue(event.target.value);
  };
  
  return (
    <TextField
      label="Controlled"
      value={value}
      onChange={handleChange}
    />
  );
}

Uncontrolled - Using the defaultValue prop or refs:


import { useRef } from 'react';
import TextField from '@mui/material/TextField';

function UncontrolledTextField() {
  const inputRef = useRef(null);
  
  const handleSubmit = () => {
    console.log(inputRef.current.value);
  };
  
  return (
    <>
      <TextField
        label="Uncontrolled"
        defaultValue="Initial value"
        inputRef={inputRef}
      />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}

In most form scenarios, especially with validation libraries like React Hook Form, controlled inputs are preferred for better control over the form state.

Customization Options

TextField offers extensive customization options:

  1. Using the sx prop - The most direct way to customize a TextField:

<TextField
  label="Custom Styled"
  sx={{
    '& .MuiOutlinedInput-root': {
      borderRadius: '8px',
      '&:hover fieldset': {
        borderColor: 'primary.main',
      },
    },
    '& .MuiInputLabel-root': {
      color: 'secondary.main',
    },
    width: '300px',
  }}
/>
  1. Theme customization - Override TextField defaults in your theme:

const theme = createTheme({
  components: {
    MuiTextField: {
      styleOverrides: {
        root: {
          '& .MuiOutlinedInput-root': {
            borderRadius: '8px',
          },
        },
      },
      defaultProps: {
        variant: 'filled',
        size: 'small',
      },
    },
  },
});
  1. Styled API - Create custom styled components:

import { styled } from '@mui/material/styles';
import TextField from '@mui/material/TextField';

const CustomTextField = styled(TextField)(({ theme }) => ({
  '& .MuiOutlinedInput-root': {
    borderRadius: '8px',
    backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#f5f5f5',
  },
  '& .MuiInputLabel-root': {
    fontWeight: 'bold',
  },
}));

// Usage:
<CustomTextField label="Custom Component" />

Accessibility Features

TextField components come with built-in accessibility features:

  1. Labels - Always use the label prop for screen readers
  2. Error states - Use error and helperText to communicate validation errors
  3. Required fields - The required prop adds the appropriate aria attributes
  4. Focus management - Proper focus handling for keyboard navigation

For enhanced accessibility, you can add additional ARIA attributes through InputProps:


<TextField
  label="Email"
  InputProps={{
    'aria-describedby': 'email-description',
  }}
/>
<div id="email-description" style={{ position: 'absolute', height: 1, width: 1, overflow: 'hidden' }}>
  Please enter your email address in the format: name@example.com
</div>

Understanding React Hook Form and Yup

Before we dive into building our form, let's understand the validation libraries we'll be using.

React Hook Form

React Hook Form is a performant, flexible form validation library with a focus on efficient rendering and easy integration. It minimizes re-renders and provides a simple API for form validation.

Key features include:

  • Minimal re-renders
  • Uncontrolled components by default (better performance)
  • Easy integration with UI libraries
  • Built-in validation
  • Simple error handling

Yup

Yup is a schema validation library that makes it easy to define validation rules. It integrates seamlessly with React Hook Form and provides:

  • Declarative schema definition
  • Rich validation rules
  • Custom error messages
  • Type inference

Setting Up the Project

Let's start by setting up our project with all the necessary dependencies.

Installation

First, create a new React project if you don't have one already:


npx create-react-app signup-form
cd signup-form

Now, install the required dependencies:


npm install @mui/material @emotion/react @emotion/styled
npm install react-hook-form @hookform/resolvers yup

Basic Project Structure

Let's organize our project structure:


src/
  ├── components/
  │   ├── SignupForm.jsx
  │   └── FormSuccess.jsx
  ├── schemas/
  │   └── validationSchema.js
  ├── App.js
  └── index.js

Creating the Validation Schema with Yup

Let's start by defining our validation schema. This will determine the rules for each field in our signup form.

Create a file at src/schemas/validationSchema.js:


import * as yup from 'yup';

// Password regex - at least 8 characters, one uppercase, one lowercase, one number
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)[a-zA-Zd]{8,}$/;

export const signupSchema = yup.object().shape({
  firstName: yup
    .string()
    .required('First name is required')
    .min(2, 'First name must be at least 2 characters')
    .max(50, 'First name cannot exceed 50 characters'),
  
  lastName: yup
    .string()
    .required('Last name is required')
    .min(2, 'Last name must be at least 2 characters')
    .max(50, 'Last name cannot exceed 50 characters'),
  
  email: yup
    .string()
    .required('Email is required')
    .email('Please enter a valid email address'),
  
  password: yup
    .string()
    .required('Password is required')
    .matches(
      passwordRegex,
      'Password must contain at least 8 characters, one uppercase, one lowercase, and one number'
    ),
  
  confirmPassword: yup
    .string()
    .required('Please confirm your password')
    .oneOf([yup.ref('password'), null], 'Passwords must match'),
  
  terms: yup
    .boolean()
    .oneOf([true], 'You must accept the terms and conditions')
});

This schema defines validation rules for:

  • First and last name (required, min/max length)
  • Email (required, valid format)
  • Password (required, complex pattern)
  • Password confirmation (must match password)
  • Terms acceptance (must be checked)

The schema uses Yup's declarative API to define the shape of our form data and the validation rules for each field.

Building the Signup Form Component

Now, let's create our signup form component that uses MUI TextField with React Hook Form and our Yup validation schema.

Create a file at src/components/SignupForm.jsx:


import { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import {
  Box,
  Button,
  TextField,
  Typography,
  Paper,
  Grid,
  Checkbox,
  FormControlLabel,
  FormHelperText,
  IconButton,
  InputAdornment,
  CircularProgress
} from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material';
import { signupSchema } from '../schemas/validationSchema';

const SignupForm = ({ onSuccess }) => {
  const [showPassword, setShowPassword] = useState(false);
  const [showConfirmPassword, setShowConfirmPassword] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const { 
    control, 
    handleSubmit, 
    formState: { errors }, 
    reset 
  } = useForm({
    resolver: yupResolver(signupSchema),
    defaultValues: {
      firstName: '',
      lastName: '',
      email: '',
      password: '',
      confirmPassword: '',
      terms: false
    }
  });

  const togglePasswordVisibility = () => {
    setShowPassword(!showPassword);
  };

  const toggleConfirmPasswordVisibility = () => {
    setShowConfirmPassword(!showConfirmPassword);
  };

  const onSubmit = async (data) => {
    setIsSubmitting(true);
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1500));
      console.log('Form submitted successfully:', data);
      reset();
      onSuccess && onSuccess(data);
    } catch (error) {
      console.error('Submission error:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <Paper elevation={3} sx={{ p: 4, maxWidth: 600, mx: 'auto', mt: 4 }}>
      <Typography variant="h4" component="h1" gutterBottom align="center">
        Create Your Account
      </Typography>
      
      <Box component="form" onSubmit={handleSubmit(onSubmit)} noValidate>
        <Grid container spacing={2}>
          {/* First Name Field */}
          <Grid item xs={12} sm={6}>
            <Controller
              name="firstName"
              control={control}
              render={({ field }) => (
                <TextField
                  {...field}
                  label="First Name"
                  variant="outlined"
                  fullWidth
                  required
                  error={!!errors.firstName}
                  helperText={errors.firstName?.message}
                  disabled={isSubmitting}
                  InputProps={{
                    'aria-describedby': 'firstName-error',
                  }}
                />
              )}
            />
          </Grid>
          
          {/* Last Name Field */}
          <Grid item xs={12} sm={6}>
            <Controller
              name="lastName"
              control={control}
              render={({ field }) => (
                <TextField
                  {...field}
                  label="Last Name"
                  variant="outlined"
                  fullWidth
                  required
                  error={!!errors.lastName}
                  helperText={errors.lastName?.message}
                  disabled={isSubmitting}
                  InputProps={{
                    'aria-describedby': 'lastName-error',
                  }}
                />
              )}
            />
          </Grid>
          
          {/* Email Field */}
          <Grid item xs={12}>
            <Controller
              name="email"
              control={control}
              render={({ field }) => (
                <TextField
                  {...field}
                  label="Email Address"
                  variant="outlined"
                  fullWidth
                  required
                  type="email"
                  error={!!errors.email}
                  helperText={errors.email?.message}
                  disabled={isSubmitting}
                  InputProps={{
                    'aria-describedby': 'email-error',
                  }}
                />
              )}
            />
          </Grid>
          
          {/* Password Field */}
          <Grid item xs={12}>
            <Controller
              name="password"
              control={control}
              render={({ field }) => (
                <TextField
                  {...field}
                  label="Password"
                  variant="outlined"
                  fullWidth
                  required
                  type={showPassword ? 'text' : 'password'}
                  error={!!errors.password}
                  helperText={errors.password?.message}
                  disabled={isSubmitting}
                  InputProps={{
                    'aria-describedby': 'password-error',
                    endAdornment: (
                      <InputAdornment position="end">
                        <IconButton
                          aria-label="toggle password visibility"
                          onClick={togglePasswordVisibility}
                          edge="end"
                        >
                          {showPassword ? <VisibilityOff /> : <Visibility />}
                        </IconButton>
                      </InputAdornment>
                    ),
                  }}
                />
              )}
            />
          </Grid>
          
          {/* Confirm Password Field */}
          <Grid item xs={12}>
            <Controller
              name="confirmPassword"
              control={control}
              render={({ field }) => (
                <TextField
                  {...field}
                  label="Confirm Password"
                  variant="outlined"
                  fullWidth
                  required
                  type={showConfirmPassword ? 'text' : 'password'}
                  error={!!errors.confirmPassword}
                  helperText={errors.confirmPassword?.message}
                  disabled={isSubmitting}
                  InputProps={{
                    'aria-describedby': 'confirmPassword-error',
                    endAdornment: (
                      <InputAdornment position="end">
                        <IconButton
                          aria-label="toggle confirm password visibility"
                          onClick={toggleConfirmPasswordVisibility}
                          edge="end"
                        >
                          {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
                        </IconButton>
                      </InputAdornment>
                    ),
                  }}
                />
              )}
            />
          </Grid>
          
          {/* Terms and Conditions */}
          <Grid item xs={12}>
            <Controller
              name="terms"
              control={control}
              render={({ field }) => (
                <FormControlLabel
                  control={
                    <Checkbox
                      {...field}
                      color="primary"
                      disabled={isSubmitting}
                    />
                  }
                  label="I agree to the terms and conditions"
                />
              )}
            />
            {errors.terms && (
              <FormHelperText error>{errors.terms.message}</FormHelperText>
            )}
          </Grid>
          
          {/* Submit Button */}
          <Grid item xs={12}>
            <Button
              type="submit"
              fullWidth
              variant="contained"
              color="primary"
              size="large"
              disabled={isSubmitting}
              sx={{ mt: 2 }}
            >
              {isSubmitting ? (
                <CircularProgress size={24} color="inherit" />
              ) : (
                'Sign Up'
              )}
            </Button>
          </Grid>
        </Grid>
      </Box>
    </Paper>
  );
};

export default SignupForm;

Let's create a simple success component to display after form submission:


// src/components/FormSuccess.jsx
import { Box, Typography, Button, Paper, CheckCircle } from '@mui/material';

const FormSuccess = ({ onReset }) => {
  return (
    <Paper elevation={3} sx={{ p: 4, maxWidth: 600, mx: 'auto', mt: 4 }}>
      <Box display="flex" flexDirection="column" alignItems="center" textAlign="center">
        <CheckCircle color="success" sx={{ fontSize: 60, mb: 2 }} />
        <Typography variant="h4" gutterBottom>
          Registration Successful!
        </Typography>
        <Typography variant="body1" paragraph>
          Your account has been created successfully. You should receive a confirmation email shortly.
        </Typography>
        <Button 
          variant="contained" 
          color="primary" 
          onClick={onReset}
          sx={{ mt: 2 }}
        >
          Start Over
        </Button>
      </Box>
    </Paper>
  );
};

export default FormSuccess;

Finally, let's update our App.js to use these components:


// src/App.js
import { useState } from 'react';
import { ThemeProvider, createTheme, CssBaseline, Container } from '@mui/material';
import SignupForm from './components/SignupForm';
import FormSuccess from './components/FormSuccess';

const theme = createTheme({
  palette: {
    primary: {
      main: '#1976d2',
    },
    secondary: {
      main: '#dc004e',
    },
  },
});

function App() {
  const [formSubmitted, setFormSubmitted] = useState(false);
  
  const handleFormSuccess = (data) => {
    setFormSubmitted(true);
    console.log('Signup data:', data);
  };
  
  const handleReset = () => {
    setFormSubmitted(false);
  };

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Container>
        {formSubmitted ? (
          <FormSuccess onReset={handleReset} />
        ) : (
          <SignupForm onSuccess={handleFormSuccess} />
        )}
      </Container>
    </ThemeProvider>
  );
}

export default App;

Understanding the Implementation

Let's break down the key elements of our implementation:

Form Setup with React Hook Form

We set up the form using React Hook Form's useForm hook with the Yup resolver:


const { 
  control, 
  handleSubmit, 
  formState: { errors }, 
  reset 
} = useForm({
  resolver: yupResolver(signupSchema),
  defaultValues: {
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: '',
    terms: false
  }
});

This provides:

  • Form state management
  • Validation using our Yup schema
  • Error tracking
  • Form reset capability

Integrating MUI TextField with React Hook Form

We use the Controller component from React Hook Form to connect MUI's TextField to our form state:


<Controller
  name="email"
  control={control}
  render={({ field }) => (
    <TextField
      {...field}
      label="Email Address"
      variant="outlined"
      fullWidth
      required
      type="email"
      error={!!errors.email}
      helperText={errors.email?.message}
      disabled={isSubmitting}
      InputProps={{
        'aria-describedby': 'email-error',
      }}
    />
  )}
/>

This pattern allows us to:

  1. Connect the TextField to React Hook Form's state
  2. Display validation errors
  3. Maintain accessibility
  4. Handle form submission state

Password Field with Toggle Visibility

For password fields, we implement a visibility toggle:


<TextField
  {...field}
  label="Password"
  variant="outlined"
  fullWidth
  required
  type={showPassword ? 'text' : 'password'}
  error={!!errors.password}
  helperText={errors.password?.message}
  InputProps={{
    endAdornment: (
      <InputAdornment position="end">
        <IconButton
          aria-label="toggle password visibility"
          onClick={togglePasswordVisibility}
          edge="end"
        >
          {showPassword ? <VisibilityOff /> : <Visibility />}
        </IconButton>
      </InputAdornment>
    ),
  }}
/>

This enhances user experience by allowing them to check their password entry while maintaining security.

Form Submission and Loading State

We handle form submission with a loading state:


const onSubmit = async (data) => {
  setIsSubmitting(true);
  
  try {
    // Simulate API call
    await new Promise(resolve => setTimeout(resolve, 1500));
    console.log('Form submitted successfully:', data);
    reset();
    onSuccess && onSuccess(data);
  } catch (error) {
    console.error('Submission error:', error);
  } finally {
    setIsSubmitting(false);
  }
};

// Submit button with loading state
<Button
  type="submit"
  fullWidth
  variant="contained"
  color="primary"
  size="large"
  disabled={isSubmitting}
>
  {isSubmitting ? (
    <CircularProgress size={24} color="inherit" />
  ) : (
    'Sign Up'
  )}
</Button>

This provides visual feedback during form submission and prevents multiple submissions.

Advanced Form Features

Now that we have our basic form working, let's explore some advanced features and customizations.

Custom TextField Styling

Let's create a custom styled TextField component:


// src/components/CustomTextField.jsx
import { styled } from '@mui/material/styles';
import TextField from '@mui/material/TextField';

const CustomTextField = styled(TextField)(({ theme }) => ({
  '& .MuiOutlinedInput-root': {
    borderRadius: theme.shape.borderRadius * 2,
    transition: theme.transitions.create(['border-color', 'box-shadow']),
    '&.Mui-focused': {
      boxShadow: `0 0 0 2px ${theme.palette.primary.light}`,
    },
  },
  '& .MuiInputLabel-root': {
    fontWeight: 500,
  },
  '& .MuiOutlinedInput-notchedOutline': {
    borderWidth: 2,
  },
}));

export default CustomTextField;

Now we can use this custom component in our form instead of the standard TextField.

Password Strength Indicator

Let's add a password strength indicator to enhance user feedback:


// Add this to SignupForm.jsx
import { useState, useEffect } from 'react';
import { LinearProgress } from '@mui/material';

// Inside the component:
const [passwordStrength, setPasswordStrength] = useState(0);

// Function to calculate password strength
const calculatePasswordStrength = (password) => {
  if (!password) return 0;
  
  let strength = 0;
  
  // Length check
  if (password.length >= 8) strength += 25;
  
  // Character type checks
  if (/[A-Z]/.test(password)) strength += 25; // Has uppercase
  if (/[a-z]/.test(password)) strength += 25; // Has lowercase
  if (/[0-9]/.test(password)) strength += 25; // Has number
  
  return strength;
};

// Update password strength when password changes
useEffect(() => {
  const subscription = control._formValues.password && 
    setPasswordStrength(calculatePasswordStrength(control._formValues.password));
  
  return () => subscription;
}, [control._formValues.password]);

// Get color based on strength
const getStrengthColor = (strength) => {
  if (strength < 50) return 'error';
  if (strength < 100) return 'warning';
  return 'success';
};

// Add this below the password TextField:
<Box sx={{ mt: 1, mb: 2 }}>
  <LinearProgress 
    variant="determinate" 
    value={passwordStrength} 
    color={getStrengthColor(passwordStrength)} 
    sx={{ height: 8, borderRadius: 4 }}
  />
  <Typography variant="caption" sx={{ mt: 0.5, display: 'block' }}>
    {passwordStrength < 50 && 'Weak password'}
    {passwordStrength >= 50 && passwordStrength < 100 && 'Medium password'}
    {passwordStrength === 100 && 'Strong password'}
  </Typography>
</Box>

Form Field Autofocus

For better user experience, let's autofocus the first field when the form loads:


// Modify the firstName Controller:
<Controller
  name="firstName"
  control={control}
  render={({ field }) => (
    <TextField
      {...field}
      label="First Name"
      variant="outlined"
      fullWidth
      required
      error={!!errors.firstName}
      helperText={errors.firstName?.message}
      disabled={isSubmitting}
      autoFocus // Add this line
      InputProps={{
        'aria-describedby': 'firstName-error',
      }}
    />
  )}
/>

Real-time Validation

By default, React Hook Form validates on submit. Let's add real-time validation as the user types:


// Modify the useForm call:
const { 
  control, 
  handleSubmit, 
  formState: { errors }, 
  reset 
} = useForm({
  resolver: yupResolver(signupSchema),
  defaultValues: {
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    confirmPassword: '',
    terms: false
  },
  mode: 'onChange' // Add this line for real-time validation
});

This will trigger validation as the user types, providing immediate feedback.

Form Accessibility Enhancements

Let's improve the accessibility of our form:

ARIA Attributes and Screen Reader Support


// Add these to the form element
<Box 
  component="form" 
  onSubmit={handleSubmit(onSubmit)} 
  noValidate
  aria-labelledby="signup-form-title"
  role="form"
>
  <Typography 
    variant="h4" 
    component="h1" 
    gutterBottom 
    align="center"
    id="signup-form-title"
  >
    Create Your Account
  </Typography>
  
  {/* Rest of the form */}
</Box>

Keyboard Navigation

Ensure all interactive elements are properly tabbable:


// For the password visibility toggle:
<IconButton
  aria-label="toggle password visibility"
  onClick={togglePasswordVisibility}
  edge="end"
  tabIndex={0} // Ensure it's in the tab order
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      togglePasswordVisibility();
    }
  }}
>
  {showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>

Error Announcements for Screen Readers

Add a live region to announce errors to screen reader users:


// Add this at the top of your form
<div 
  aria-live="assertive" 
  role="alert"
  style={{ position: 'absolute', height: 1, width: 1, overflow: 'hidden' }}
>
  {Object.values(errors).length > 0 && 
    `Form has ${Object.values(errors).length} error${Object.values(errors).length > 1 ? 's' : ''}`
  }
</div>

Best Practices and Common Issues

Let's cover some best practices and common issues when working with MUI TextField in forms:

Performance Optimization

  1. Memoization: Use React.memo for form components that don't need to re-render frequently
  2. Debouncing: For real-time validation, debounce the validation to prevent excessive re-renders

// Install lodash
// npm install lodash

import { debounce } from 'lodash';
import { useCallback } from 'react';

// Inside your component:
const debouncedValidation = useCallback(
  debounce((value) => {
    // Your validation logic here
    console.log('Validating:', value);
  }, 300),
  []
);

// Usage:
<TextField
  onChange={(e) => {
    field.onChange(e);
    debouncedValidation(e.target.value);
  }}
  // other props
/>

Common Issues and Solutions

1. Form Resets Not Working

If your form reset isn't working properly, make sure you're using the reset function from useForm and that you're resetting all fields:


// Complete reset with default values
reset({
  firstName: '',
  lastName: '',
  email: '',
  password: '',
  confirmPassword: '',
  terms: false
});

2. Validation Errors Not Clearing

If validation errors aren't clearing after fixing the input, check your validation mode:


// Try different validation modes
const { ... } = useForm({
  // ...
  mode: 'onChange', // Validates on change
  // or
  mode: 'onBlur', // Validates when field loses focus
  // or
  mode: 'onTouched', // Validates after first blur
});

3. TextField Not Updating

If your TextField isn't updating with new values, make sure you're spreading the field props correctly:


<Controller
  name="email"
  control={control}
  render={({ field }) => (
    <TextField
      {...field} // This spreads onChange, onBlur, value, etc.
      // other props
    />
  )}
/>

4. Handling Conditional Fields

For conditional fields that depend on other field values:


// Get watch function to observe field values
const { control, watch } = useForm();
const userType = watch('userType');

// Then in your JSX:
{userType === 'business' && (
  <Controller
    name="companyName"
    control={control}
    render={({ field }) => (
      <TextField
        {...field}
        label="Company Name"
        // other props
      />
    )}
  />
)}

Best Practices

  1. Consistent Error Handling: Use the same pattern for displaying errors across all fields
  2. Form Submission Feedback: Always provide visual feedback during submission
  3. Field Grouping: Group related fields together (e.g., first and last name)
  4. Progressive Disclosure: Show complex fields only when needed
  5. Validation Timing: Choose the right validation timing for your use case (onChange, onBlur, onSubmit)
  6. Accessibility First: Always design with accessibility in mind
  7. Responsive Design: Ensure your form works well on all device sizes

Advanced Form Layout and Styling

Let's enhance our form with a more professional layout and styling:


// Enhanced SignupForm.jsx with advanced styling
import { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import {
  Box,
  Button,
  TextField,
  Typography,
  Paper,
  Grid,
  Checkbox,
  FormControlLabel,
  FormHelperText,
  IconButton,
  InputAdornment,
  CircularProgress,
  Divider,
  Stepper,
  Step,
  StepLabel,
  useTheme,
  useMediaQuery
} from '@mui/material';
import { Visibility, VisibilityOff, Person, Email, Lock } from '@mui/icons-material';
import { signupSchema } from '../schemas/validationSchema';

const SignupForm = ({ onSuccess }) => {
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
  
  const [showPassword, setShowPassword] = useState(false);
  const [showConfirmPassword, setShowConfirmPassword] = useState(false);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [activeStep, setActiveStep] = useState(0);
  
  const steps = ['Personal Info', 'Account Details', 'Confirmation'];
  
  const { 
    control, 
    handleSubmit, 
    formState: { errors, isValid }, 
    reset,
    trigger
  } = useForm({
    resolver: yupResolver(signupSchema),
    defaultValues: {
      firstName: '',
      lastName: '',
      email: '',
      password: '',
      confirmPassword: '',
      terms: false
    },
    mode: 'onChange'
  });

  const togglePasswordVisibility = () => {
    setShowPassword(!showPassword);
  };

  const toggleConfirmPasswordVisibility = () => {
    setShowConfirmPassword(!showConfirmPassword);
  };

  const handleNext = async () => {
    let fieldsToValidate = [];
    
    if (activeStep === 0) {
      fieldsToValidate = ['firstName', 'lastName'];
    } else if (activeStep === 1) {
      fieldsToValidate = ['email', 'password', 'confirmPassword'];
    }
    
    const isStepValid = await trigger(fieldsToValidate);
    
    if (isStepValid) {
      setActiveStep((prevStep) => prevStep + 1);
    }
  };

  const handleBack = () => {
    setActiveStep((prevStep) => prevStep - 1);
  };

  const onSubmit = async (data) => {
    setIsSubmitting(true);
    
    try {
      // Simulate API call
      await new Promise(resolve => setTimeout(resolve, 1500));
      console.log('Form submitted successfully:', data);
      reset();
      onSuccess && onSuccess(data);
    } catch (error) {
      console.error('Submission error:', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <Paper 
      elevation={3} 
      sx={{ 
        p: { xs: 2, sm: 4 }, 
        maxWidth: 700, 
        mx: 'auto', 
        mt: 4,
        borderRadius: 2,
        background: theme.palette.mode === 'dark' 
          ? 'linear-gradient(145deg, #2d2d2d 0%, #1a1a1a 100%)' 
          : 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)'
      }}
    >
      <Typography 
        variant="h4" 
        component="h1" 
        gutterBottom 
        align="center"
        sx={{ 
          fontWeight: 700,
          mb: 3,
          background: 'linear-gradient(45deg, #1976d2 30%, #21CBF3 90%)',
          WebkitBackgroundClip: 'text',
          WebkitTextFillColor: 'transparent'
        }}
      >
        Create Your Account
      </Typography>
      
      <Stepper 
        activeStep={activeStep} 
        alternativeLabel={!isMobile}
        orientation={isMobile ? 'vertical' : 'horizontal'}
        sx={{ mb: 4 }}
      >
        {steps.map((label) => (
          <Step key={label}>
            <StepLabel>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
      
      <Box component="form" onSubmit={handleSubmit(onSubmit)} noValidate>
        {activeStep === 0 && (
          <Grid container spacing={2}>
            <Grid item xs={12}>
              <Typography variant="h6" gutterBottom>
                Personal Information
              </Typography>
              <Divider sx={{ mb: 2 }} />
            </Grid>
            
            {/* First Name Field */}
            <Grid item xs={12} sm={6}>
              <Controller
                name="firstName"
                control={control}
                render={({ field }) => (
                  <TextField
                    {...field}
                    label="First Name"
                    variant="outlined"
                    fullWidth
                    required
                    error={!!errors.firstName}
                    helperText={errors.firstName?.message}
                    disabled={isSubmitting}
                    autoFocus
                    InputProps={{
                      'aria-describedby': 'firstName-error',
                      startAdornment: (
                        <InputAdornment position="start">
                          <Person color="primary" />
                        </InputAdornment>
                      ),
                    }}
                  />
                )}
              />
            </Grid>
            
            {/* Last Name Field */}
            <Grid item xs={12} sm={6}>
              <Controller
                name="lastName"
                control={control}
                render={({ field }) => (
                  <TextField
                    {...field}
                    label="Last Name"
                    variant="outlined"
                    fullWidth
                    required
                    error={!!errors.lastName}
                    helperText={errors.lastName?.message}
                    disabled={isSubmitting}
                    InputProps={{
                      'aria-describedby': 'lastName-error',
                      startAdornment: (
                        <InputAdornment position="start">
                          <Person color="primary" />
                        </InputAdornment>
                      ),
                    }}
                  />
                )}
              />
            </Grid>
            
            {/* Navigation Buttons */}
            <Grid item xs={12} sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end' }}>
              <Button
                variant="contained"
                onClick={handleNext}
                sx={{ minWidth: 100 }}
              >
                Next
              </Button>
            </Grid>
          </Grid>
        )}
        
        {activeStep === 1 && (
          <Grid container spacing={2}>
            <Grid item xs={12}>
              <Typography variant="h6" gutterBottom>
                Account Details
              </Typography>
              <Divider sx={{ mb: 2 }} />
            </Grid>
            
            {/* Email Field */}
            <Grid item xs={12}>
              <Controller
                name="email"
                control={control}
                render={({ field }) => (
                  <TextField
                    {...field}
                    label="Email Address"
                    variant="outlined"
                    fullWidth
                    required
                    type="email"
                    error={!!errors.email}
                    helperText={errors.email?.message}
                    disabled={isSubmitting}
                    autoFocus
                    InputProps={{
                      'aria-describedby': 'email-error',
                      startAdornment: (
                        <InputAdornment position="start">
                          <Email color="primary" />
                        </InputAdornment>
                      ),
                    }}
                  />
                )}
              />
            </Grid>
            
            {/* Password Field */}
            <Grid item xs={12}>
              <Controller
                name="password"
                control={control}
                render={({ field }) => (
                  <TextField
                    {...field}
                    label="Password"
                    variant="outlined"
                    fullWidth
                    required
                    type={showPassword ? 'text' : 'password'}
                    error={!!errors.password}
                    helperText={errors.password?.message}
                    disabled={isSubmitting}
                    InputProps={{
                      'aria-describedby': 'password-error',
                      startAdornment: (
                        <InputAdornment position="start">
                          <Lock color="primary" />
                        </InputAdornment>
                      ),
                      endAdornment: (
                        <InputAdornment position="end">
                          <IconButton
                            aria-label="toggle password visibility"
                            onClick={togglePasswordVisibility}
                            edge="end"
                          >
                            {showPassword ? <VisibilityOff /> : <Visibility />}
                          </IconButton>
                        </InputAdornment>
                      ),
                    }}
                  />
                )}
              />
            </Grid>
            
            {/* Confirm Password Field */}
            <Grid item xs={12}>
              <Controller
                name="confirmPassword"
                control={control}
                render={({ field }) => (
                  <TextField
                    {...field}
                    label="Confirm Password"
                    variant="outlined"
                    fullWidth
                    required
                    type={showConfirmPassword ? 'text' : 'password'}
                    error={!!errors.confirmPassword}
                    helperText={errors.confirmPassword?.message}
                    disabled={isSubmitting}
                    InputProps={{
                      'aria-describedby': 'confirmPassword-error',
                      startAdornment: (
                        <InputAdornment position="start">
                          <Lock color="primary" />
                        </InputAdornment>
                      ),
                      endAdornment: (
                        <InputAdornment position="end">
                          <IconButton
                            aria-label="toggle confirm password visibility"
                            onClick={toggleConfirmPasswordVisibility}
                            edge="end"
                          >
                            {showConfirmPassword ? <VisibilityOff /> : <Visibility />}
                          </IconButton>
                        </InputAdornment>
                      ),
                    }}
                  />
                )}
              />
            </Grid>
            
            {/* Navigation Buttons */}
            <Grid item xs={12} sx={{ mt: 2, display: 'flex', justifyContent: 'space-between' }}>
              <Button
                onClick={handleBack}
                sx={{ minWidth: 100 }}
              >
                Back
              </Button>
              <Button
                variant="contained"
                onClick={handleNext}
                sx={{ minWidth: 100 }}
              >
                Next
              </Button>
            </Grid>
          </Grid>
        )}
        
        {activeStep === 2 && (
          <Grid container spacing={2}>
            <Grid item xs={12}>
              <Typography variant="h6" gutterBottom>
                Confirm & Submit
              </Typography>
              <Divider sx={{ mb: 2 }} />
            </Grid>
            
            {/* Terms and Conditions */}
            <Grid item xs={12}>
              <Controller
                name="terms"
                control={control}
                render={({ field }) => (
                  <FormControlLabel
                    control={
                      <Checkbox
                        {...field}
                        color="primary"
                        disabled={isSubmitting}
                      />
                    }
                    label={
                      <Typography variant="body2">
                        I agree to the{' '}
                        <Typography
                          component="span"
                          variant="body2"
                          color="primary"
                          sx={{ cursor: 'pointer', textDecoration: 'underline' }}
                        >
                          terms and conditions
                        </Typography>
                      </Typography>
                    }
                  />
                )}
              />
              {errors.terms && (
                <FormHelperText error>{errors.terms.message}</FormHelperText>
              )}
            </Grid>
            
            {/* Submit Button */}
            <Grid item xs={12} sx={{ mt: 2 }}>
              <Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
                <Button
                  onClick={handleBack}
                  sx={{ minWidth: 100 }}
                >
                  Back
                </Button>
                <Button
                  type="submit"
                  variant="contained"
                  color="primary"
                  size="large"
                  disabled={isSubmitting || !isValid}
                  sx={{ minWidth: 150 }}
                >
                  {isSubmitting ? (
                    <CircularProgress size={24} color="inherit" />
                  ) : (
                    'Complete Signup'
                  )}
                </Button>
              </Box>
            </Grid>
          </Grid>
        )}
      </Box>
    </Paper>
  );
};

export default SignupForm;

This enhanced version includes:

  1. A multi-step form with a stepper
  2. Improved styling with gradients and icons
  3. Responsive design for mobile and desktop
  4. Better visual hierarchy with typography and dividers
  5. Step validation before proceeding

Wrapping Up

In this comprehensive guide, we've explored how to build a robust, validated signup form using MUI's TextField component with React Hook Form and Yup. We've covered everything from basic setup to advanced features and best practices.

The combination of MUI's polished UI components, React Hook Form's efficient state management, and Yup's powerful validation creates a developer and user-friendly form solution. This approach gives you the best of all worlds: great-looking forms, excellent performance, and robust validation.

Remember that forms are often the first interaction users have with your application, so investing time in creating a smooth, error-free experience pays dividends in user satisfaction and conversion rates. The techniques we've covered can be applied to any form scenario, from simple contact forms to complex multi-step workflows.