Dynamic forms and form validation in React JS

Anton Ioffe - August 31st 2023 - 16 minutes read

Introduction to Dynamic Forms in React JS

Dynamic forms, as opposed to static ones, are a crucial element in many web applications, especially when built using a modern framework such as React JS. The inherent complexity and fluidity of such forms can be a challenge to tackle, but when approached right, dynamic forms offer efficiency, flexibility, and adaptability that traditional static forms cannot compete with.

In simple terms, a dynamic form is one where the form fields are not fixed but can change based on user interaction or other conditions. In contrast, static forms have fields that remain constant and do not change in the lifetime of the form. The dynamic nature of such forms makes them a preferred choice for achieving a highly interactive user interface, which plays a pivotal role in ensuring a positive user experience (UX).

React JS, a popular, powerful JavaScript library, is widely adopted for building dynamic UIs for web applications, thus making it an ideal choice for creating dynamic forms. With its robust ecosystem and features such as the virtual DOM, it provides an efficient, flexible approach to manage the state of form fields and helps in creating complex forms with varied dependencies and conditions.

The creation of dynamic forms in React JS inherently taps into several advantages of the framework. React's component-oriented architecture allows for a clean design of a form containing different individual form-field components. Thus, a specific form-field component could be reused in different parts of the form, or even different forms, enhancing the code's modularity and reusability.

Conceptually, working with dynamic forms in React JS involves dealing with an array of form field definitions rather than static markup. The application's state or specific user interactions define the addition and removal of form fields, their ordering, and even their validation requirements. This paves the way for creating highly customizable and user-friendly forms, which can adapt to a wide range of user inputs and conditions.

In summary, dynamic forms are an important part of modern web applications, and they provide a flexible and interactive way of gathering data. When developed using React JS, they leverage the powerful features of the JavaScript library to offer a highly efficient and reusable solution. While static forms still have their place for simple, unchangeable data input requirements, dynamic forms provide the much-needed adaptability for user interfaces designed for complex, real-world scenarios.

Creating Dynamic Forms in React JS

Dynamic forms provide flexibility to handle different sets of data and inputs. We'll discuss how you can create dynamic forms in React JS and handle dynamic data.

Let's start with an empty array. This will hold all our form elements data. We'll use the useState hook for this.

import React, { useState } from 'react';

function App() {
    const [form, setForm] = useState([]);
}

Now, let's imagine you have a button that when clicked on, it adds a new form element to your form. Let's implement this.

function App() {
    const [form, setForm] = useState([]);

    const addElement = () => {
       const newElement = { id: Math.random(), data: '' };

       // Using the spread operator (...) to make a copy of the form array
       setForm([...form, newElement]);
    }

    return (
        <div className="app">
            <button onClick={addElement}>Add Input</button>
        </div>
    );
}

We assign a random unique id to each element to avoid any collisions. Also, we're using 'data' key for each element to store its input.

Now, let's render the form element.

function App() {
    const [form, setForm] = useState([]);

    const addElement = () => {
        // ...
    }

    const handleInputChange = (e, id) => {
        const newForm = form.map((item) => {
            return item.id === id ? { ...item, data: e.target.value } : item;
        });

        setForm(newForm);
    }

    return (
        <div className="app">
            <button onClick={addElement}>Add Input</button>
            {form.map((item) => (
                <input
                  key={item.id}
                  onChange={(e) => handleInputChange(e, item.id)}
                  value={item.data}
                />
            ))}
        </div>
    );
}

In this piece of code, we are mapping over the form array to render an input for each object in the array. We store the input's value in the object 'data' property. When the input's value changes, we use handleInputChange to update the 'data' property with the new value. We pass the object's id along with the event to this function.

The handleInputChange function loops over the form array looking for the object with the id that matches the id of the input field that triggered the event. It then updates the value of the 'data' property for the matched object.

By following these steps, you can create a dynamic form in ReactJS that populates based on the state of your application. Adding more form elements or types won't require major changes to your structure, making your form component maintainable and scalable. Remember to always use unique keys for your dynamic elements to ensure React can efficiently re-render your components.

Handling Forms in React JS

Understanding form handling in React is essential due to the ubiquitous nature of forms in web applications. Two crucial components to consider during form handling in React are the form fields and form data.

We will look at a simple form with a username and email address field in React. Note that we are using JSX syntax here, which is a JavaScript syntax extension provided by React.

function MyForm() {
  return (
    <form>
      <label>
        Username:
        <input type="text" name="username" />
      </label>
      <label>
        Email:
        <input type="text" name="email" />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

This is a typical HTML form structure. As it stands, there are no mechanisms to manage the form data or handle its submission. Let's see how to address this.

In React, form handling often utilizes the concept of 'state'. A common approach is using the state to hold and update the form fields' values as they change.

Let's initialise the state for each field using the useState hook:

import React, { useState } from "react";
function MyForm() {
  const [username, setUsername] = useState("");
  const [email, setEmail] = useState("");

  // The actual form would follow here
}

Next, we bind the state variables to the input fields by setting up an 'onChange' event handler:

<label>
  Username:
  <input
    type="text"
    name="username"
    value={username}
    onChange={e => setUsername(e.target.value)}
  />
</label>
<label>
  Email:
  <input
    type="text"
    name="email"
    value={email}
    onChange={e => setEmail(e.target.value)}
  />
</label>

So, every time a user interacts with the form fields, the component state is updated. However, it's a common pitfall to miss ensuring that the state updates with every change in the form, causing inconsistencies.

Here's a flawed code example which does not update the state as expected:

<label>
  Username:
  <input
    type="text"
    name="username"
    value={username}
    onChange={() => setUsername("fixed_value")} // Error: this does not reflect the user input
  />
</label>

Next is submission of the form data. In React, a common approach is to prevent the browser from refreshing the page on form submission, which is the default behaviour. This is done using e.preventDefault() in the onSubmit event handler as shown below:

function MyForm() {
  // states initialization and other associated code

  const handleSubmit = (event) => {
    event.preventDefault();
    // form submission logic goes here
    console.log({username, email});
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields go here */}
    </form>
  );
}

An often-seen mistake when dealing with form submission in React is neglecting to prevent the browser's default behaviour. This leads to the page refreshing every time the form is submitted, which typically isn't desired in single-page React applications.

Here's an example of this mistake:

function MyForm() {
  // states initialization and other associated code

  const handleSubmit = (event) => {
    // forgetting to prevent default behaviour
    console.log({username, email});
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* form fields go here */}
    </form>
  );
}

And that's it! We have covered the basic concepts of form handling in React. There are certainly more complex scenarios and additional layers of complexity that we could explore, but we'll save those for future conversations.

Various Types of Forms and Form Validation

Forms play an essential role in digital interactions, as they allow users to input data, perform transactions, and communicate with applications. A well-designed and efficient form validation not only helps to ensure that the right data gets entered into your forms but also improves the user experience by providing useful feedback. Below is an exploration of various types of forms and form validation techniques.

These forms can be divided into two main types: single-page forms and multi-page forms. The former is quite uncomplicated, with all the fields present on a single page, while the latter divides the fields across multiple pages or steps, improving clarity for multifaceted forms.

Form validation is the technique of confirming if a form has been filled in correctly before it is processed. We have two familiar approaches in Javascript and Typescript - client-side and server-side validations.

Client-side validation, using vanilla JavaScript or libraries like React, inspects data on a client’s browser as they press the submit button. It is quick and decreases server load.

Here is an example of client-side validation in vanilla JavaScript:

const form = document.getElementById('form');
const email = document.getElementById('email');

form.addEventListener('submit', (e) => {
    let messages = [];

    if (email.value === '' || email.value == null) {
        messages.push('Email is required');
    }

    if (messages.length > 0) {
        e.preventDefault();
        console.error(messages.join(', '));
    }
});

In this example, an error message is displayed if the user tries to submit the form with an empty email field.

Server-side validation, as the term suggests, is performed on the server side. It’s a secure form of validation as it conceals the validation rules defined by the developer and also offers a secondary layer of protection for data besides client-side validation.

// Server-side validation with Express in Nodejs
router.post('/submit', (req, res) => {
    let email = req.body.email;

    if (email === '' || email == null) {
        res.status(400).send('Email is required');
    } else {
        res.status(200).send('Form submission successful');
    }
});

In this code snippet, server-side validation with Express is demonstrated. If the email field is empty, an HTTP status code 400 is transmitted with an error message. Otherwise, the server responds with a success message.

Validation in ReactJS forms mirrors vanilla JS, but the form fields are typically surveyed during the onChange event, followed by final validation when the form is submitted. Libraries like Formik or React-Hook-Form can assist in managing the form state and validation.

import React, { useState } from 'react';

function MyForm() {
    const [email, setEmail] = useState('');

    const handleSubmit = (event) => {
        event.preventDefault();

        if (!email) {
            console.error('Email is required');
        }
    };

    const handleChange = (event) => {
        setEmail(event.target.value);
    };

    return (
        <form onSubmit={handleSubmit}>
            <input type='email' value={email} onChange={handleChange}/>

            <button type='submit'>Submit</button>
        </form>
    );
}

The React component shown above is a single-page form with an email field. The onChange event updates the email in the component's state, and when the form is submitted, the handleSubmit function executes the validation.

However, it's not uncommon to overlook the proper syntax for regex email validation. Using the right regex pattern that strictly adheres to the standard format is key to having a robust form validation system.

Let's consider an example of improper regex usage for email validation:

//Incorrect regex
const email = 'example.com';
const regex = /[A-Z0-9._%+-]+@[A-Z0-9-]+.+.[A-Z]{2,4}/gim;

const isValidEmail = regex.test(email);
if (!isValidEmail) {
    console.error('Email is not valid');
}

In the above example, the regex pattern is incorrect because it does not check for the presence of '@' and '.' required for any valid email address. This kind of mistake can lead to unsuccessful validations.

By adopting these validation techniques, we can enhance the user experience and ensure data integrity. It's worth noting that regardless of how sophisticated your backend may be, a web application would struggle without solid client-side validations. Using proper regex for validations is a significant aspect of this practice, so always be cautious about its implementation while dealing with forms.

Form Validation using JavaScript and React JS

Form validation is a crucial component in modern web development. It ensures the user's input meets the required format and type. With React JS and JavaScript, we are spoilt for choice when it comes to front-end validation. In the myriad of choices, regex (regular expressions) and custom validation techniques stand out for their effectiveness.

For clarity, custom validation, in this context, refers to unique checks designed by the developer using JavaScript. These checks ensure the structure or content of the data meets the expected standards.

It's essential not to overlook the exhaustive validation of all form fields. Failure to do so can result in complications, especially when dealing with edge cases. These edge cases involve unexpected or unusual inputs that differ from the so-called 'standard' input values. Think of a situation where a user enters special characters where only alphanumeric inputs are expected, or perhaps a user inputs a tremendously lengthy string potentially causing data overflow.

In the examples below, we will maintain clarity and brevity by focusing on the JavaScript validation code and not the full JSX implementation.

Let's examine a simple example of a form in React which contains username, email, and password input fields:

import { useState } from 'react';

function RegisterForm() {
  const [values, setValues] = useState({ username: '', email: '', password: '' });
  const [errors, setErrors] = useState({});

  const handleChange = (event) => {
    if (event.target.type !== 'text') {
      return;
    }

    setValues({ ...values, [event.target.name]: event.target.value.trim() });
  };

  return;
}

In this code snippet, we have the useState hook creating state fields for the form values (values) and any arising errors (errors). The handleChange function is monitoring changes in the text input fields, and subsequently updating the value in the corresponding values state field.

Let's introduce the validate function for custom validation:

function validate(values) {
  let errors = {};

  // Validating username
  if (!values.username) {
    errors.username = 'Username is required';
  } else if (values.username.length <= 3) {
    errors.username = 'Username must be longer than 3 characters';
  }

  // Validating password
  if (!values.password) {
    errors.password = 'Password is required';
  } else if (values.password.length <= 6) {
    errors.password = 'Password must be longer than 6 characters';
  }

  // Validating email, with regex
  const re = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i;
  if (!values.email) {
    errors.email = 'Email is required';
  } else if (!re.test(values.email)) {
    errors.email = 'Invalid email address';
  }

  return errors;
}

In this part of the code, the validate function assesses each form field according to our predefined rules, populating the errors object with any discrepancies encountered during validation.

The email address validation uses a regex, which is a string of characters defining a search pattern. In our case, it checks whether the email adheres to the general format of a valid email address. These patterns can become intricate, depending on the specific requirements.

Here's a challenge for you: Extend the 'validate' function with a 'confirm password' field that ties to the password field in the state. However, this validation should only be carried out once all form fields have been filled.

And here's the solution:

// (...)
// Validating confirm password
if (!values.confirmPassword) {
    errors.confirmPassword = 'Confirming the password is required';
} else if (values.confirmPassword !== values.password) {
    errors.confirmPassword = 'Passwords do not match';
}
// (...)

In conclusion, the fusion of regex and custom validation in React paves the way for thorough and efficient scrutiny and management of user input. It offers constructive feedback when the input data does not align with the expected format, thereby enhancing the reliability and security of the applications we build in React.

Form Styling and Extras

Correct form styling contributes significantly to your application aesthetics. Deploying distinct features correctly can significantly elevate the user experience. Hence, in this guide, we will delve into JavaScript, TypeScript, and ReactJS nuances. We will detail how these languages can successfully be utilized in form styling.

Understanding Form Styling

Form styling is fundamental to ensure a seamless and enjoyable user experience. Style aspects such as colors, borders, backgrounds, and interactive effects—like hover and focus states—can notably enhance user experience.

One common mistake often made during form styling is adding styles within the JavaScript or TypeScript files instead of separating code and design.

Consider this poor practice example:

// A bad practice – styles added within the JavaScript file
const myFormElement = document.getElementById('myForm'); 
myFormElement.style.backgroundImage = 'url(https://example.com/background.png)';
myFormElement.style.fontSize = '14px';

worse yet, combining styles within the same file can lead to bloated, hard-to-read configurations. Instead, by utilizing external CSS files, JavaScript and TypeScript can apply classes ensuring a streamlined workflow where style and logic are separated. This manner of separating concerns leads to more sustainable, manageable code.

Form Styling with JavaScript and TypeScript

Here is the correct way to use external CSS files in JavaScript and TypeScript for styling:

// Attaching styles from an external CSS file in JavaScript
const myFormElement = document.getElementById('myForm');
// Add the CSS class to the HTML form element
myFormElement.classList.add('myFormStyle');
// Attaching styles from an external CSS file in TypeScript
const myFormElement: HTMLFormElement = document.getElementById('myForm') as HTMLFormElement;
// Add the CSS class to the HTML form element
myFormElement.classList.add('myFormStyle');

In these examples, the variable myFormElement refers to the HTML form element. Consequently, myFormElement.classList.add('myFormStyle') applies the CSS class myFormStyle to this form element, ensuring that styles and code are kept separate.

Form Styling with ReactJS

ReactJS form styling differs slightly. It supports styling through CSS classes, but employs the className prop instead of class. It can lead to a common mistake, using 'class' in React components. For instance, the wrong way to do it:

// Mistake– using 'class' instead of 'className' in a React component
<input class='myFormStyle' />

The correct way to do it is:

// Correct – uses 'className' to assign a CSS class in React
<input className='myFormStyle' />

Responsiveness in Form Styling

Design consistency is crucial in form styling. The form should look great across all screen sizes. For width and height properties, use relative units like percentages, ems, or viewport units. This ensures scalable design across different device screens.

Properties like padding, margin, and overflow play a vital role in creating a responsive design. Here is the correct way to use these properties:

// Responsiveness achieved through relative units
.myFormStyle {
  height: 50vh;
  margin: 10%;
  padding: 10%;
  overflow: auto;
}

Beware of setting fixed heights using pixels. This common mistake can create a poor user experience on some devices:

// Ineffective practice – using fixed units
.myFormStyle {
  height: 500px; // Not responsive
}

Exercise: Now, your goal is to implement a responsive form using pure CSS. Only use CSS, JavaScript, or TypeScript, avoiding any external libraries or state management tools. Create a form that provides a delightful user experience on both mobile and desktop devices.

Tip: Start by creating a basic form with several input fields and a submission button. Apply CSS rules subsequently to make your form responsive across various screen sizes.

The Best Practices for Form Handling and Validation

Form handling and validation play a vital role in any web application, and it's critical to follow best practices. This article delves into these best practices, primarily focusing on Javascript, Typescript, and React.js.

Recognizing the Importance of Form Handling

In form handling, we chiefly record user input and manage the form's state. This state contains the form fields' values and the submission status.

Gathering User Input

React.js's onChange event handler offers an efficient means to gather user input. The state of the form updates with every keystroke or form selection. Consider the following comprehensive code example:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }
  // Other parts of the component code...
}

In the preceding example, handleChange is the event handler that updates the form's state according to the user input.

Controlling Form State

Controlling the form's state can become complex, as it involves tracking the form fields' values and other facets like the status of the Submit button, according to the server's feedback. Here, React.js and the useState hook prove instrumental in managing the state of functional components. The following code illustrates how to manage a form submission's state:

function MyFormComponent() {
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async event => {
    event.preventDefault(); 
    setIsSubmitting(true);

    await new Promise(resolve => setTimeout(resolve, 2000));

    setIsSubmitting(false);
  };
  // Rendering and other parts of the code...
}

Best Practices in Form Validation

Validation is of utmost importance in form handling. It verifies if the input provided by the user meets specific predefined criteria before forwarding the values to the server.

Inline Validation

Inline validation validates form fields as the user finishes entering data, offering immediate feedback and enhancing the user experience. The below code validates an email field:

function MyFormComponent() {
  const [email, setEmail] = useState('');
  const [emailError, setEmailError] = useState('');

  const handleBlur = () => {
    if (!email.includes('@')) {
      setEmailError('Please enter a valid email!');
    }
  };

  // Rendering and other parts of the code...
}

In the above snippet, handleBlur links with the onBlur event of the email input field. This function confirms the '@' inclusion in the email value, updating the error message if it's absent.

Error Handling

Never underestimate error handling. One of the best practices to handle errors post form submissions is to intercept server errors and display clear messages to users. Let's dissect an example using a try/catch block:

function MyFormComponent() {
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');

  const handleSubmit = async e => {
    e.preventDefault();
    setIsSubmitting(true);

    try {
      await new Promise((_, reject) => setTimeout(() => reject('Invalid input!'), 2000));
    } catch (error) {
      setErrorMessage(error);
    } finally {
      setIsSubmitting(false);
    }
  };
  // Rendering and other parts of the code...
}

Proper error handling makes your application more robust and user-friendly. It also ensures that any unexpected circumstances, such as network issues or incorrect user entry, won't result in a crash or confusing outcomes for the user.

The Impact of Best Practices in Form Handling

Adopting robust form handling and validation best practices significantly enhances user experience and improves your application's overall quality. Accurate user input recording, efficient form state tracking, and effective error management prove to be beneficial. They enhance the application's reliability, bolster user satisfaction, and reduce potential bugs and errors leading to more maintainable code.

Don't Get Left Behind:
The Top 5 Career-Ending Mistakes Software Developers Make
FREE Cheat Sheet for Software Developers