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! 😉
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 theid
to find out the DOM element in theevent
parameterevent.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 unalteredThe way we can build strings in run-time is known as
interpolation
. An example of how to do it is shown in theconsole.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.