Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to create reusable form components in React

Forms are the backbone of most web applications, but managing repetitive boilerplate for every input field can quickly lead to an unmaintainable codebase. With over 25 years of software development experience and as the creator of CoreUI, I’ve architected dozens of high-performance form systems for complex enterprise dashboards. The most efficient approach involves building a suite of primitive components that encapsulate styling, accessibility, and validation logic while remaining flexible via props. This modular strategy is exactly what we use to build our React Dashboard Template for maximum developer productivity.

Create reusable form components by extracting common logic into functional components that accept standard HTML attributes and custom props like label or error.

1. Creating a Reusable Input Component

The first step is to create a base input component. This component should handle the label, the input field itself, and any validation messages. By using a standardized wrapper, you ensure that every text input in your application follows the same visual and functional pattern.

import React from 'react'
import { CFormInput, CFormLabel, CFormFeedback } from '@coreui/react'

const FormInput = ({ label, id, error, ...props }) => {
  // This component wraps the standard CoreUI CFormInput
  // providing a consistent layout for labels and errors.
  return (
    <div className='mb-3'>
      {label && (
        <CFormLabel htmlFor={id}>
          {label}
        </CFormLabel>
      )}
      <CFormInput
        id={id}
        invalid={!!error}
        {...props}
      />
      {error && (
        <CFormFeedback invalid>
          {error}
        </CFormFeedback>
      )}
    </div>
  )
}

export default FormInput

This snippet uses the CoreUI React Input documentation to leverage pre-styled, accessible components. By spreading ...props, we allow the parent component to pass standard attributes like type, placeholder, or onChange directly to the underlying input.

2. Implementing a Reusable Select Component

Select menus often share the same layout requirements as text inputs. Instead of rewriting the container logic, you can mirror the structure of your input component to create a consistent user experience.

import React from 'react'
import { CFormSelect, CFormLabel, CFormFeedback } from '@coreui/react'

const FormSelect = ({ label, id, options = [], error, ...props }) => {
  // We map through the options array to render select choices
  // ensuring the first option is usually a placeholder.
  return (
    <div className='mb-3'>
      {label && (
        <CFormLabel htmlFor={id}>
          {label}
        </CFormLabel>
      )}
      <CFormSelect
        id={id}
        invalid={!!error}
        {...props}
      >
        {options.map((opt) => (
          <option key={opt.value} value={opt.value}>
            {opt.label}
          </option>
        ))}
      </CFormSelect>
      {error && (
        <CFormFeedback invalid>
          {error}
        </CFormFeedback>
      )}
    </div>
  )
}

export default FormSelect

For more advanced selection needs, you can refer to the CoreUI React Select documentation. This pattern makes it incredibly easy to swap a standard select for a custom one without changing the overall form structure.

3. Handling Checkboxes and Radios

Checkboxes and radio buttons require a slightly different layout because the label is usually positioned next to the input rather than above it. Creating a specific component for these types ensures your alignment is always perfect.

import React from 'react'
import { CFormCheck, CFormFeedback } from '@coreui/react'

const FormCheckbox = ({ label, id, error, ...props }) => {
  // CFormCheck handles the layout of the checkbox and its label
  // internally, keeping our component code very clean.
  return (
    <div className='mb-3'>
      <CFormCheck
        id={id}
        label={label}
        invalid={!!error}
        {...props}
      />
      {error && (
        <CFormFeedback invalid>
          {error}
        </CFormFeedback>
      )}
    </div>
  )
}

export default FormCheckbox

Consistency in error reporting is key. Even if the visual layout changes, the way you pass and display errors should remain predictable across all form components.

4. Supporting Ref Forwarding for Third-Party Libraries

When using libraries like React Hook Form, you often need access to the underlying DOM node. Using React.forwardRef allows your reusable components to work seamlessly with these tools.

import React, { forwardRef } from 'react'
import { CFormInput, CFormLabel, CFormFeedback } from '@coreui/react'

const FormField = forwardRef(({ label, id, error, ...props }, ref) => {
  // Forwarding the ref is essential for integration with
  // validation libraries that need to focus the element on error.
  return (
    <div className='mb-3'>
      {label && <CFormLabel htmlFor={id}>{label}</CFormLabel>}
      <CFormInput
        id={id}
        ref={ref}
        invalid={!!error}
        {...props}
      />
      {error && <CFormFeedback invalid>{error}</CFormFeedback>}
    </div>
  )
})

FormField.displayName = 'FormField'

export default FormField

This is a best practice we strictly follow in CoreUI to ensure our components are “library-agnostic” and easy to integrate into any stack.

5. Composition of a Complete Form

Once your primitives are ready, building a complex form becomes a matter of composition. This significantly reduces the lines of code in your feature components and makes global changes (like updating a theme) trivial.

import React, { useState } from 'react'
import { CButton, CForm } from '@coreui/react'
import FormInput from './FormInput'
import FormSelect from './FormSelect'

const RegistrationForm = () => {
  const [formData, setFormData] = useState({ email: '', role: '' })
  const [errors, setErrors] = useState({})

  const handleSubmit = (e) => {
    e.preventDefault()
    // Simple validation example
    if (!formData.email) {
      setErrors({ email: 'Email is required' })
    }
  }

  return (
    <CForm onSubmit={handleSubmit}>
      <FormInput
        label='Email Address'
        id='email'
        type='email'
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
        error={errors.email}
      />
      <FormSelect
        label='User Role'
        id='role'
        options={[
          { label: 'Select a role...', value: '' },
          { label: 'Admin', value: 'admin' },
          { label: 'User', value: 'user' }
        ]}
        value={formData.role}
        onChange={(e) => setFormData({ ...formData, role: e.target.value })}
      />
      <CButton type='submit' color='primary'>
        Register
      </CButton>
    </CForm>
  )
}

By abstracting the form controls, the RegistrationForm component only cares about data and business logic, not the intricacies of CSS or HTML structure.

6. Managing Dynamic Validation Logic

For larger applications, you might want to centralize validation logic. You can pass validation results into your reusable components to keep the UI in sync with the state.

import React from 'react'
import FormInput from './FormInput'

const ValidationExample = () => {
  const [email, setEmail] = React.useState('')
  
  // You might use a utility to check if the string is empty
  const getError = (val) => {
    if (!val) return 'This field cannot be empty'
    return null
  }

  return (
    <FormInput
      label='Validated Input'
      value={email}
      onChange={(e) => setEmail(e.target.value)}
      error={getError(email)}
      placeholder='Type something...'
    />
  )
}

Integrating logic like this helps you avoid common pitfalls. For instance, you can check if a field is required using an internal helper or reference our guide on /answers/how-to-check-if-a-string-is-empty-in-javascript/ for precise validation.

Best Practice Note:

Always ensure your reusable components support the disabled and readOnly states. In CoreUI, we provide these states out of the box, but when wrapping them, you must ensure these props are passed down correctly to maintain accessibility. Additionally, consider using the CoreUI Validation documentation to learn about browser-native vs. custom validation styles. Separating your form layout from your business logic is the single most effective way to scale a React application.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team