Max Chereza Logo

Forms

Top of the morning!

Our previous discussion explored the benefits of using JSX to define the appearance of our application. We challenged ourselves to transition from executing React top-level API functions to JSX notation.

This time and building on top of the acquired knowledge, we will concentrate on forms and how to manage their inputs. The great news is that if you're already familiar with working with forms in JavaScript, React will be straightforward. If not, don't worry; we've got you covered! 😉

Blogpost Hero Image

The Form Component

Let's start with a simple example. We'll have an input for the user's homeland and a Submit button:

1function HomeLandForm() {
2  return (
3    <form>
4      <div>
5        <label htmlFor="userHomeLand">
6          Where are you from?
7        </label>
8
9        <input
10          type="text"
11          id="userHomeLand"
12        />
13      </div>
14
15      <button type="submit">Submit</button>
16    </form>
17  );
18};

For our component, as it is, when the user presses the Submit button, the browser redirects them to a new page. However, that may not be the behaviour we are looking for. To prevent it we can assign a function to the onSubmit property in the <form> tag to control the flow after submitting. The function will handle the entered data and define the next steps:

1const HomeLandForm = () => {
2  const handleSubmit = (event) => {
3    console.log(`Value Submitted: ${event.target.userHomeLand.value}`);
4    event.preventDefault();
5  };
6
7  return (
8    <form onSubmit={handleSubmit}>
9      <div>
10        <label htmlFor="userHomeLand">Where are you from? </label>
11        <input
12          type="text"
13          id="userHomeLand"
14        />
15      </div>
16
17      <button type="submit">Submit</button>
18    </form>
19  );
20};
21

A couple of points I would like to comment on here:

  • Check out how we are getting the value entered in the text input as event.target.userHomeLand.value. We can use the id to find out the DOM element in the event parameter

  • event.preventDefault() will prevent the default browser behaviour and avoid any default redirects. This is a usual practice as the form and the values entered must remain on-screen and unaltered

  • The way we can build strings in run-time is known as interpolation. An example of how to do it is shown in the console.log statement

Right now, we're getting our input value controls ready at the submit level, which isn't a big deal because this example only has one. I'm pointing this out because there's nothing between the form and the handleSubmit function. Imagine how messy it might get if ten or fifteen uncontrolled inputs overload our function.

Wouldn’t it be better to allow every input to control itself so that when the user presses the submit button we can make sure about the sanity level of the data we are handling?

Controlled Elements

The plan is to create a mechanism that allows retrieval of useful information about the entered data and triggering of side routines based on the entered data or other events.

1<input type="text" id="userHomeLand" value="Uruguay" />

In this snippet of code, the <input> is currently set to "Uruguay", and if we try to change it, nothing will happen on the screen. That's because the input is uncontrolled for us, and its value is based on the value property. So, what makes a component considered controlled? If we pass a variable to value, it means that the component releases its control to us, and we are now responsible for managing the variable and deciding how it should change along the life cycle.

What we need is a method to control the current state of the component at any time, and make decisions based on how the variable changes:

1<input type="text" id="userHomeLand" value={value} />

Can React help us keep track of and access state variables? Absolutely! In this post, we'll cover the basics of setting up a form component. In the next one, we'll get into it, discover how it works, and learn how to use it effectively to solve all sorts of challenges.

State Management

When our component has a state, we can keep track of certain variables and react accordingly. Basically, we can save the value of our text input, check if it's valid, show an error message, or enable another input in the same form.

1function HomeLandForm() {
2  //This is how we define a state variable
3  //The value is stored in homeLand, and we can use setHomeLand to alter the state value
4  const [homeLand, setHomeLand] = useState("");
5
6  //When the user submits, we work with the newest state value
7  const handleSubmit = (event) => {
8    console.log(`Value Submitted: ${homeLand}`);
9    event.preventDefault();
10  };
11
12  //Every time the user alters the input, we save the new value in the state
13  const handleChange = (event) => {
14    setHomeLand(event.target.value);
15  };
16
17  return (
18    <form onSubmit={handleSubmit}>
19      <div>
20        <label htmlFor="userHomeLand">Where are you from?</label>
21        <input
22          type="text"
23          id="userHomeLand"
24          onChange={handleChange}
25          value={homeLand}
26        />
27      </div>
28
29      <button type="submit">Submit</button>
30    </form>
31  );
32};

In React, to keep track of state variables, we can use the useState() function. This function gives us an array where the first item is the state variable initialized with the value we provide, and the second item is a function to update that state variable. We are meant to use this function to update the state.

As a value for the <input>, we assign the state variable instead of a hardcoded value. This makes the input a controlled component, and we manage it using the state, which is initially an empty string (''). This is why you will find the input empty on the screen.

The onChange event in the input will run every time its value changes. We can use this trigger to update the state variable using the setter function, setHomeLand. Now you can change the value entered and see with your eyes the current state of <input>. What is happening is that a component refreshes on the screen every time any of its props gets its value updated.

Validate the Input

As the controllers of the <input> component, we can decide whether or not to update its value. For example, at this point whatever the user types will update the state, but we are dealing with country names here, maybe cities. Why not validate that the first character of the string is capitalised?

1  const handleChange = (event) => {
2    const inputValue = event.target.value;
3    const firstChar = inputValue.charAt(0);
4    firstChar.toUpperCase() === firstChar && setHomeLand(inputValue);
5  };

For every change to <input>, we check its first character using charAt() to confirm it is capitalised. Only in case it is, we execute setHomeLand(inputValue).

Reusability

Imagine two groups interested in using our component. Group A wants to validate the first character as we already did (is capitalised?), though Group B requests to validate the input by executing their custom function groupB_Validation(). How do you think we could upgrade our form to satisfy both requirements?

In React, when we want to make a component more reusable, we usually let the user customise the component behaviour specifically for a need. In other words, we should allow either Group A or Group B to provide different onChange methods to the component. The way we have to give that flexibility is through props:

1function HomeLandForm({handleChange}) {  
2  return (
3    <form onSubmit={handleSubmit}>
4      <div>
5        <label htmlFor="userHomeLand">Where are you from?</label>
6        <input
7          type="text"
8          id="userHomeLand"
9          onChange={handleChange}
10          value={homeLand}
11        />
12      </div>
13
14      <button type="submit">Submit</button>
15    </form>
16  );
17};
18
19const handleChange = (event) => {
20  const inputValue = event.target.value;
21  const firstChar = inputValue.charAt(0);
22  firstChar.toUpperCase() === firstChar && setHomeLand(inputValue);
23};
24
25//Group A is happy with the original implementation
26<HomeLandForm handleChange={handleChange} />
27
28//Group B will make use of the component using their own secret function
29<HomeLandForm handleChange={groupB_Validation} />

<HomeLandForm> will execute the function provided in the handleChange prop when the onChange event trigger. This is enough for both groups to validate the entry as they please, but we can go even further and allow them not just to define how the <input> validates, but also what should happen when submitting the form and even what is its homeLand value.

If we elevate all the moving parts and make the component receive them as props, it will become much more usable for many more use cases. This can also be beneficial, as there would not be much business logic in our component, making its code much cleaner and easier to maintain!

Summary

Today, we learned about handling forms in React, building on what we already know about JSX to define our app's appearance. We started with a simple form to collect the user's homeland and handle form submissions as needed. Then, we moved on to controlled components, where we figured out how to manage input values by using React's state management with the useState hook.

We focused on the importance of controlled components by explaining how to capture and validate user input effectively. For example, we learned how to ensure that the first character of the input is in uppercase before updating the state. We also talked about the significance of making components reusable by passing only what they need through props, allowing different groups to, for instance, implement their custom validation logic.

The Extra Mile

Let's make our form component more interesting.

It could be more informative for the user if we explain why nothing happens when starting the input value with a lowercase character. In earlier challenges, we created a <Text> component to display a "Hello" message on the screen. We could use it to show a red error message below the <input> when an invalid value is entered.

Here’s a snippet of the <Text> component in case it helps:

1const Text = (props) => {
2  const {style, message} = {...props}
3
4  return <span style={style}>
5    {message ? message : props.children}
6  </span>
7}

Feel free to play around with this example, including my solution for the challenge, here. Next time, we will work with Arrays and learn how to manipulate them in React.