CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-formik

Build forms in React, without the tears

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

error-handling.mddocs/

Error Handling

Components and patterns for displaying validation errors with flexible rendering options.

Capabilities

ErrorMessage Component

Component for displaying field error messages with flexible rendering patterns.

/**
 * Component for displaying field error messages
 * @param props - ErrorMessage configuration
 * @returns JSX element displaying error or null if no error
 */
function ErrorMessage(props: ErrorMessageProps): JSX.Element | null;

interface ErrorMessageProps {
  /** Name of the field to display errors for */
  name: string;
  /** Component or HTML element to render error with */
  component?: React.ComponentType<any> | keyof JSX.IntrinsicElements;
  /** Children render function receiving error message */
  children?: (error: string) => React.ReactNode;
  /** Render prop function receiving error message */
  render?: (error: string) => React.ReactNode;
  /** CSS class name for styling */
  className?: string;
  /** Additional HTML attributes */
  [key: string]: any;
}

Usage Examples:

import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from "yup";

// Basic usage with default div element
const BasicForm = () => (
  <Formik
    initialValues={{ email: '', password: '' }}
    validationSchema={Yup.object({
      email: Yup.string().email('Invalid email').required('Required'),
      password: Yup.string().min(6, 'Too short').required('Required'),
    })}
    onSubmit={values => console.log(values)}
  >
    <Form>
      <Field name="email" type="email" />
      <ErrorMessage name="email" />
      
      <Field name="password" type="password" />
      <ErrorMessage name="password" />
      
      <button type="submit">Submit</button>
    </Form>
  </Formik>
);

// With custom component
const StyledError = ({ children }) => (
  <div className="error-message" style={{ color: 'red', fontSize: '12px' }}>
    {children}
  </div>
);

const StyledForm = () => (
  <Formik
    initialValues={{ username: '' }}
    validate={values => {
      const errors = {};
      if (!values.username) {
        errors.username = 'Username is required';
      }
      return errors;
    }}
    onSubmit={values => console.log(values)}
  >
    <Form>
      <Field name="username" />
      <ErrorMessage name="username" component={StyledError} />
      <button type="submit">Submit</button>
    </Form>
  </Formik>
);

// With render prop
const RenderPropForm = () => (
  <Formik
    initialValues={{ age: '' }}
    validate={values => {
      const errors = {};
      if (!values.age) {
        errors.age = 'Age is required';
      } else if (values.age < 18) {
        errors.age = 'Must be 18 or older';
      }
      return errors;
    }}
    onSubmit={values => console.log(values)}
  >
    <Form>
      <Field name="age" type="number" />
      <ErrorMessage
        name="age"
        render={error => (
          <div className="alert alert-danger">
            <strong>Error:</strong> {error}
          </div>
        )}
      />
      <button type="submit">Submit</button>
    </Form>
  </Formik>
);

// With children function
const ChildrenFunctionForm = () => (
  <Formik
    initialValues={{ phone: '' }}
    validate={values => {
      const errors = {};
      if (values.phone && !/^\d{10}$/.test(values.phone)) {
        errors.phone = 'Phone must be 10 digits';
      }
      return errors;
    }}
    onSubmit={values => console.log(values)}
  >
    <Form>
      <Field name="phone" placeholder="Phone number" />
      <ErrorMessage name="phone">
        {error => (
          <div className="error-tooltip">
            <span className="icon">⚠️</span>
            <span className="message">{error}</span>
          </div>
        )}
      </ErrorMessage>
      <button type="submit">Submit</button>
    </Form>
  </Formik>
);

// With HTML element and styling
const HtmlElementForm = () => (
  <Formik
    initialValues={{ website: '' }}
    validate={values => {
      const errors = {};
      if (values.website && !/^https?:\/\/.+/.test(values.website)) {
        errors.website = 'Website must be a valid URL';
      }
      return errors;
    }}
    onSubmit={values => console.log(values)}
  >
    <Form>
      <Field name="website" placeholder="Website URL" />
      <ErrorMessage
        name="website"
        component="span"
        className="field-error"
        style={{ color: 'red', fontStyle: 'italic' }}
      />
      <button type="submit">Submit</button>
    </Form>
  </Formik>
);

Error Display Patterns

Common patterns for displaying and handling form errors.

Field-Level Error Display:

import { useField } from "formik";

// Custom field component with integrated error display
const TextInputWithError = ({ label, ...props }) => {
  const [field, meta] = useField(props);
  
  return (
    <div className="form-field">
      <label htmlFor={props.id || props.name}>{label}</label>
      <input {...field} {...props} />
      {meta.touched && meta.error ? (
        <div className="error">{meta.error}</div>
      ) : null}
    </div>
  );
};

// Usage
const CustomFieldForm = () => (
  <Formik
    initialValues={{ firstName: '', lastName: '' }}
    validate={values => {
      const errors = {};
      if (!values.firstName) errors.firstName = 'Required';
      if (!values.lastName) errors.lastName = 'Required';
      return errors;
    }}
    onSubmit={values => console.log(values)}
  >
    <Form>
      <TextInputWithError
        label="First Name"
        name="firstName"
        type="text"
      />
      <TextInputWithError
        label="Last Name"
        name="lastName"
        type="text"
      />
      <button type="submit">Submit</button>
    </Form>
  </Formik>
);

Form-Level Error Summary:

const FormWithErrorSummary = () => (
  <Formik
    initialValues={{ name: '', email: '', phone: '' }}
    validate={values => {
      const errors = {};
      if (!values.name) errors.name = 'Name is required';
      if (!values.email) errors.email = 'Email is required';
      if (!values.phone) errors.phone = 'Phone is required';
      return errors;
    }}
    onSubmit={values => console.log(values)}
  >
    {({ errors, touched, isValid }) => (
      <Form>
        {/* Error Summary */}
        {!isValid && Object.keys(touched).length > 0 && (
          <div className="error-summary">
            <h4>Please fix the following errors:</h4>
            <ul>
              {Object.entries(errors).map(([field, error]) => (
                touched[field] && <li key={field}>{error}</li>
              ))}
            </ul>
          </div>
        )}
        
        <Field name="name" placeholder="Name" />
        <ErrorMessage name="name" component="div" className="field-error" />
        
        <Field name="email" type="email" placeholder="Email" />
        <ErrorMessage name="email" component="div" className="field-error" />
        
        <Field name="phone" placeholder="Phone" />
        <ErrorMessage name="phone" component="div" className="field-error" />
        
        <button type="submit">Submit</button>
      </Form>
    )}
  </Formik>
);

Async Validation Errors:

const AsyncValidationForm = () => (
  <Formik
    initialValues={{ username: '' }}
    validate={async (values) => {
      const errors = {};
      
      if (!values.username) {
        errors.username = 'Username is required';
      } else if (values.username.length < 3) {
        errors.username = 'Username must be at least 3 characters';
      } else {
        // Simulate async validation
        try {
          const available = await checkUsernameAvailability(values.username);
          if (!available) {
            errors.username = 'Username is already taken';
          }
        } catch (error) {
          errors.username = 'Error checking username availability';
        }
      }
      
      return errors;
    }}
    onSubmit={values => console.log(values)}
  >
    {({ isValidating }) => (
      <Form>
        <Field name="username" placeholder="Username" />
        {isValidating && <span>Checking availability...</span>}
        <ErrorMessage name="username" component="div" className="error" />
        <button type="submit">Submit</button>
      </Form>
    )}
  </Formik>
);

const checkUsernameAvailability = async (username) => {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000));
  return !['admin', 'user', 'test'].includes(username.toLowerCase());
};

Server Error Handling:

const ServerErrorForm = () => (
  <Formik
    initialValues={{ email: '', password: '' }}
    onSubmit={async (values, { setSubmitting, setFieldError, setStatus }) => {
      try {
        await loginUser(values);
        setStatus({ type: 'success', message: 'Login successful!' });
      } catch (error) {
        if (error.field) {
          // Field-specific error
          setFieldError(error.field, error.message);
        } else {
          // General error
          setStatus({ type: 'error', message: error.message });
        }
      } finally {
        setSubmitting(false);
      }
    }}
  >
    {({ status, isSubmitting }) => (
      <Form>
        {/* Global status messages */}
        {status && (
          <div className={`alert alert-${status.type}`}>
            {status.message}
          </div>
        )}
        
        <Field name="email" type="email" placeholder="Email" />
        <ErrorMessage name="email" component="div" className="error" />
        
        <Field name="password" type="password" placeholder="Password" />
        <ErrorMessage name="password" component="div" className="error" />
        
        <button type="submit" disabled={isSubmitting}>
          {isSubmitting ? 'Logging in...' : 'Login'}
        </button>
      </Form>
    )}
  </Formik>
);

const loginUser = async (credentials) => {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  if (credentials.email === 'taken@example.com') {
    throw { field: 'email', message: 'Email already registered' };
  }
  
  if (credentials.password === 'wrong') {
    throw { field: 'password', message: 'Incorrect password' };
  }
  
  if (!credentials.email || !credentials.password) {
    throw { message: 'Please fill in all fields' };
  }
  
  return { success: true };
};

Array Field Errors:

const ArrayErrorForm = () => (
  <Formik
    initialValues={{ emails: [''] }}
    validate={values => {
      const errors = {};
      
      if (!values.emails || values.emails.length === 0) {
        errors.emails = 'At least one email is required';
      } else {
        const emailErrors = [];
        values.emails.forEach((email, index) => {
          if (!email) {
            emailErrors[index] = 'Email is required';
          } else if (!/\S+@\S+\.\S+/.test(email)) {
            emailErrors[index] = 'Invalid email format';
          }
        });
        
        if (emailErrors.length > 0) {
          errors.emails = emailErrors;
        }
      }
      
      return errors;
    }}
    onSubmit={values => console.log(values)}
  >
    {({ values, errors, touched }) => (
      <Form>
        <FieldArray name="emails">
          {({ push, remove }) => (
            <div>
              {values.emails.map((email, index) => (
                <div key={index}>
                  <Field name={`emails.${index}`} placeholder="Email" />
                  {/* Individual field error */}
                  {errors.emails && 
                   errors.emails[index] && 
                   touched.emails && 
                   touched.emails[index] && (
                    <div className="error">{errors.emails[index]}</div>
                  )}
                  <button type="button" onClick={() => remove(index)}>
                    Remove
                  </button>
                </div>
              ))}
              <button type="button" onClick={() => push('')}>
                Add Email
              </button>
            </div>
          )}
        </FieldArray>
        
        {/* Array-level error */}
        {typeof errors.emails === 'string' && (
          <div className="error">{errors.emails}</div>
        )}
        
        <button type="submit">Submit</button>
      </Form>
    )}
  </Formik>
);

Error Types and Interfaces

TypeScript interfaces for error handling.

type FormikErrors<Values> = {
  [K in keyof Values]?: Values[K] extends any[]
    ? Values[K][number] extends object
      ? FormikErrors<Values[K][number]>[] | string | string[]
      : string | string[]
    : Values[K] extends object
    ? FormikErrors<Values[K]>
    : string;
};

interface ErrorMessageProps {
  /** Field name to display errors for */
  name: string;
  /** Component to render error with */
  component?: React.ComponentType<any> | keyof JSX.IntrinsicElements;
  /** Children render function */
  children?: (error: string) => React.ReactNode;
  /** Render prop function */
  render?: (error: string) => React.ReactNode;
  /** CSS class name */
  className?: string;
  /** Additional props */
  [key: string]: any;
}

docs

context-integration.md

core-components.md

error-handling.md

field-arrays.md

form-state-management.md

index.md

tile.json