Table of Contents
I hope you all are excited to finally bring interactivity to your React apps 😁
If you’ve been following along with this series, you should be up and running with:
- a project created with create-react-app
- custom JSX components
- scoped styles using CSS module
Today, we hit part four which is all about the exciting world of event handlers and working with React state.
Event handlers are essential tools that allow us to respond to user actions, such as button clicks, form submissions, or keyboard inputs.
When combined with React state, we can create components that update and re-render based on user interactions, making our apps more responsive and user-friendly.
In this blog post, we’ll cover the basics of using event handlers in React, how to handle events in functional components, and how to manage component states to create dynamic user interfaces.
So, ready your fingers and prep your keyboards to create interactive web applications that captivate your users!
React State
React runs through the code executing each function one time. It won’t reevaluate upon an event unless specified.
What this means is that if we start with a name that’s an empty string (indicating an empty name input on our form), but then want to update the name to reflect a user’s input, we can’t.
Setting a new value for the variable name
won’t cause React to update the initial value (as is the case with Vanilla JS).
Instead, we’ll use the updating function to trigger a reevaluation that will update the variable value for us.
This is how React works, it takes care of updating stuff but you gotta tell it to do so 💁♀️
As a little test run, let’s collect the name and age from the two form inputs.
- Import
useState
from “react” in the Form component file.
useState
is a React hook that you call inside of the component function.- It must run directly in the component function – you can’t place it outside of
const Form = () ⇒ {}
.
- Call
useState
inside of the component, specifying a default value. useState
returns an array of the value itself and an updating function.
- Use array destructuring to access the two returned items.
- The order is important! It’s always the case that the first returned item is a value while the second is the updating function.
let [name, setName] = useState('')
let [age, setAge] = useState('');
Read: 14 ES6 Features You Need To Know To Refresh Your JavaScript
- Execute the updating function in the handler to schedule a change on rerun.
- The updating function schedules a re-evaluation so any other lines of code following the function call get evaluated prior to the update taking effect.
- Console logging
name
orage
will yield the default values of empty strings.
const nameHandler = (e) => {
setName(e.target.value);
};
const ageHandler = (e) => {
setAge(+e.target.value);
};
Note 👀
In the sample above,e.target.value
is essentially extracting the input value. This isn’t React-specific but something we often see in Vanilla JS. Further, I use a unary operator to turn the string type ofage
into an integer.
Different ways to store React states
You can have multiple states per component like I’m doing here, or you can initialize a state with an object and group different states.
If grouping different states, keep in mind they’re managed as one piece of state so every time one piece is updated, all pieces need to be “updated” whether to new or existing values.
We can do that by spreading data of the existing state object; if we don’t, we’ll lose the data 😢
Example:
let [inputs, setInputs] = useState({ name: "", age: "" });
const nameHandler = (e) => {
// setName(e.target.value);
setInputs({...inputs, name: e.target.value})
};
const ageHandler = (e) => {
// setAge(+e.target.value);
setInputs({...inputs, age: e.target.value})
};
For the rest of the project, I’ll manage multiple states per component due to preference – you do you 👍
Event handlers with React components
React uses special “on” props for tags that represent event handlers. These props take a function as a value to execute when an event occurs.
The event handler points to a function defined outside of the return statement. They don’t execute the function to avoid premature execution (hence the omitted parenthesis).
Unlike the inline JS in HTML, React event handlers:
- Have special syntax (i.e.
onClick
instead ofonclick
) - Act as pointers to an executable function
The functions they point to conventionally end with “Handler”, though, this is unnecessary and varies across React developers.
As a little demo, let’s add onChange
events to the name and age inputs.
onChange={nameHandler}
onChange={ageHandler}
Logical walkthrough: When there’s a change in either input, the onChange
event triggers. Depending on the input affected, the pointed handler fires updating the input values to the entry value.
Similarly, listen for the form submission, onSubmit={formSubmitHandler}
, and, in the formSubmitHandler
console log the two input variables.
Child-to-parent component communication
Next, we need to get the updated inputs onto our USERS
list defined in the App component file.
The flow of the data will occur from child, Form.js, to parent, App.js. The question is, how do we get the two input variables to the App component?
Well, it’s not all that complicated. We can achieve the child-to-parent dataflow in three steps:
- On the parent, define a custom event with a value that is a handler function.
- The handler function receives data from the child, spreads it, and, optionally, adds additional data.
- In the child, execute the handler function inherited from props, passing child data to the parent as a parameter.
Note ‼️
Props can’t skip intermediary components so information flows across React components must go up or down each component of a branch.
What this looks like is this 👇
Initialize state in App.js
Set the initial state for the USERS
array and make sure the users passed onto the List component point to the newly assigned users
variable instead of USERS
.
👉 let [users, setUsers] = useState(USERS)
Define a custom event with a handler function
On the Form component, define a custom event that points to what’ll be the handler function accepting the child data.
👉 newUser={newUserDataHandler}
Follow by defining the handler like so:
const newUserDataHandler = (data) => {
// data => {name, age} from form
setUsers((currUsers) => [...currUsers, data]);
};
The function newUserDataHandler
accepts some data, then updates users
by copying the existing array using the spread operator before adding the incoming data.
Note: Updating a React State Based on an Existing State
When updating a state that depends on a previous state, as in the example above, use a callback function.
The callback automatically gets the previous state snapshot through a default argument. You can call the default argument whatever you desire, I went with currUsers
.
Here, currUsers
references the existing state of the users
. When I spread it into the new array, I’m retaining all data as it currently exists before adding the new data
.
Since USERS
is an array of objects, the new information coming from the form submission will be in the form of an object with two properties – name
and age
.
Tip: Always get the latest state as React keeps scheduled updates in mind. Otherwise, your logic might depend on outdated state!
Pass data from child-to-parent
Moving to Form.js, instead of console logging the inputs in formSubmitHandler
, let’s store them in a variable called data
as an object.
Then pass data
into props.newUser
that accepts an argument since newUser
points to a handler expecting one.
let data = { name, age };
props.newUser(data);
Heading back to App.js, if I console log the new users
we get:
Clearly, THT is the younger of the two no matter how you shuffle the integers 😔
Rundown: Define a function with a parameter in the parent component. Pass the function as a prop to a child component where you access it via props
. Execute using data as an argument.
Two-way binding
There’s one more little, yet, important bit that needs addressing on the Form component and that’s a concept called “two-way binding”.
Two-way binding refers to when you feed a state change back to an input or other component creating a controlled component.
It’s great for forms because it allows (a) to gather user input and (b) change it on submission.
Why you need two-way binding
Take our work so far as an example. We’re now successfully gathering user input, updating and storing it in a variable that we pass along to the parent component where we proceed to save it in our “database”.
But when the form is submitted, the entered values remain on the input fields prompting a user to either refresh the page or manually delete the existing inputs to enter new ones.
I’ll let you on to a little secret: Expecting a user to do much of anything other than click and enter is going too far 🤫
Instead, when the form is submitted, the inputs should clear to ready themselves for another entry. This means another state change for name
and age
after the data
is passed onto the parent.
Tip 🧐
If you update the input states prior to sending over the collecteddata
, you’ll overwrite the information with the defaults!
A completed formSubmitHandler
:
const formSubmitHandler = (e) => {
e.preventDefault();
let data = { name, age };
props.newUser(data);
setName('');
setAge('')
};
Notice it’s not enough to update the input states – the entered user info still populates the inputs. They need a reset!
We must go a step further to implement two-way binding by passing the values of name
and age
back to the inputs so they reflect current information.
A completed form element:
<form className={`${styles.form}`} onSubmit={formSubmitHandler}>
<label for="name">Name</label>
<input
type="text"
id="name"
name="name"
value={name}
onChange={nameHandler}
/>
<label for="age">Age</label>
<input
type="number"
id="age"
name="age"
max="100"
min="1"
value={age}
onChange={ageHandler}
/>
<Button type="submit">Add User</Button>
</form>
And it’s a wrap! We’re now four parts into this project 🎊
This post gave substance and made our app “work” through event handlers and data processing.
Aren’t we bloody awesome 🙃
The full source code is on GitHub, but keep a lookout for part five where we tackle rendering lists and implementing conditionals.
TTFN 👋