2 - Reacting to changes
Recap
Last week we looked at how to write a HelloMentor
React component (interactive example):
Handling events
So far we have only looked at React apps that are "static": they don't respond to user input. This week we will look at making our apps dynamic.
Recap: First-class functions in JavaScript
Before we look more at React we need to recap a concept in JavaScript. You may remember that functions in JavaScript are "first class" - that means we can pass a reference to a function (as a variable) and then call it elsewhere. Let's look at an example (interactive example):
In the example above hello
is a reference to a function. In the first console.log
we log out the whole function. The function is not called until we use parentheses ()
, so we only log the string "Hello!"
in the second console.log
.
This is a really important and useful in React, as we can make a function and pass it to React so that it can call it when a user interacts with our app.
Event handlers in components
In previous lessons we learned how to attach event listeners with addEventListener
:
We still need to listen to events in React, but event handlers are set up in a slightly different way (interactive example):
Every element in React has some special props that start with on
that can be assigned to a function which will be called when the event is triggered.
Here's a few examples (a full list is available here):
onClick
- the element was clickedonCopy
- the clipboard is used to copy some textonKeyDown
- a key is pressed downonBlur
- the element loses "focus"onChange
- only available for<input>
&<select>
(and a few others), triggered when changedonDoubleClick
- the element was double-clicked!onPlay
- a video starts playingonSubmit
- a form element is submitted
Notice that just like with addEventListener
above, we pass the function reference to onClick
instead of calling the function. If we call the function, it will run the function when we render, not when the user clicks on the button. (Remember that rendering is the term in React for inserting into the DOM).
:::tip Think of it like this: we give the event handler to React, so that React can call our function when the element is clicked. :::
Passing Functions as Props
Sometimes we need to pass a function to another component as a prop, so that it can handle the event.
A common example for this is a Button component. This component adds some styling to a normal <button>
, but still needs to be able to pass an event handler function to onClick
. Let's look at an example (interactive example):
Notice how this is very similar to the example above where we created the handler and used it in the same component? The only difference here is that we are passing the function reference through a prop. We could even pass it through multiple components as props.
Re-rendering components
So far we've seen that when the page loads, React calls our function components. The JSX elements that are returned from the component functions are turned into the DOM for you by React.
To be able to react to changes, we need to re-render our function components to get different JSX elements. React can then update the DOM based on the new JSX elements.
Let's look at how a component is re-rendered (interactive version):
If you look in the console, you'll see that the component is rendered once when the page loads. props.likeCount
starts at 0, so React inserts "Count: 0" into the DOM.
We won't look at how this works at the moment, but behind the scenes there is some code that will listen for clicks on the button and force React to update. That means when you click the button, the function component is called again (or re-rendered).
Now props.likeCount
is 1. React now updates the DOM to make sure it shows the correct number. Every time we click the button, the function component is called and React updates the DOM for us.
We don't need to worry about changing the DOM ourselves! This is what makes React so powerful. Even better, React will figure out exactly the right bits of the DOM that need to be changed, a concept called the "virtual DOM". This makes it extremely efficient and fast.
State
State is a general concept in software engineering. It is used when a part of your app needs to "remember" something that changes when people interact with it.
This is a simple example, but if we had lots of bits of state, then we can make very complex apps.
React Hooks
React has built-in functionality for initializing and updating state in our components. We will access state via a React Hook called useState
.
Hooks are a newish feature in React. You may find older tutorials that don't use Hooks, but don't panic. The concepts we learn here are the same whether or not you use Hooks. We are looking at Hooks first because they are simpler to learn for beginners.
Importing useState
useState
To be able to access the useState
Hook, we first need to import it from the React package. Let's look at an example (interactive example):
If we look at the console, useState
is just a function. It lives inside the React code that you installed when you created the app.
To reference the useState
function in our component, we need to import it from the React code. The curly braces around useState
are a bit like writing:
In fact we can just write React.useState
in our component if we want! But to type a bit less code, we import it (using the curly braces) once and then can just use useState
.
Using useState
useState
Now let's look at how we can use the useState
Hook (interactive example):
Let's break this down into small pieces. First, let's look at calling the useState
function:
This initializes the state variable to 0. Any parameter passed to useState
will be used as the initial value.
Next, let's look at how we render the state variable in our component:
count
is just a variable, so to insert it into our JSX we treat it like any other variable: we use curly braces.
Finally, let's look at how we get hold of the count
variable:
To fully understand this bit of code, we first have to understand destructuring. Let's look at this blog post by Wes Bos about array destructuring.
Now we can understand that useState
is returning an array, with two items. The first item in the array is the current value of the count
state. In our example it will be 0 on the first render. The second item in the array is a function that we will use to update our state.
Tip: Follow the useState
naming convention.
When we destructure an array, we can name the variables whatever we want, but there is a naming convention when destructuring the useState
array. The first variable should be named whatever your state is called, and the second variable should be the same name but prefixed with set
. Let's look at some examples:
Updating State
Our Counter isn't very useful right now! Let's make it more useful by getting count
to actually count up (interactive example):
Our component now has a <button>
, which will call the incrementCount
function when clicked:
The incrementCount
function then calculates the new state by adding 1 onto the current count
. And then calls setCount
to set the new state:
setCount
does two things. First, it updates the state that our component is "remembering". Whatever you pass as the argument to setCount
will be remembered as the new state.
It also tells React that the old state that is still shown in the DOM is outdated and so the DOM needs to change. Because of this, React will re-render all of our components to figure out what to change in the DOM.
When re-rendering, React will call our Counter
component again, but this time when we call useState
it will give us the updated state of 1, instead of the initial state of 0:
On the second render, count
is now set to 1. Every time we click the button, the whole cycle starts again.
Don't mutate State
As we just learned, setCount
updates the state for us, but it also notifies React of changes. If you try to just change the count
variable without using setState
, nothing will happen, because React wouldn't be notified of the change. You can only modify (or mutate) state using the setter function (interactive example):
Where does State live?
We have talked about how a component "remembers" state. In fact, each component instance remembers separate state from other components. This means we can have multiple different Counters, each with a different state (interactive example):
More complex states
The examples we've looked at so far have used numbers, strings and Booleans. You can also use arrays and objects in state too. Let's take a look at an example (interactive example).
In this shopping list example, we're initializing the list
state to be an empty array. To display our list we loop through the array (like we learned last week) and render an <li>
for each item in the list.
When we want to add something to the list, we can use the list.concat
method to make a new array with the new item. This new array is then set as the new state. Right now, our example is not very useful as it can only add Bread to the list! Next week, we'll look at how we can allow users to write their own items to the list.
Tip : Don't use the array push
method with state. Instead use the array concat
method.
The list.push
method won't work here, as this method mutates the existing array. React requires a completely new array to be set as the new state, otherwise it doesn't realize that the value has changed. The concat
method works because it copies the whole existing array to a brand new array before it adds the new item.
Setting multiple States
So far we've only seen an example with one state variable. But you can create multiple state variables if you want! Let's see an example (interactive example):
When do you use Props or State?
We've looked at the 2 main ways of managing data in our React components. But when should we use props and when should we use state?
Remember that props are like "arguments" to a component. It's good practice to make sure that you don't modify arguments after you receive them. Just like state, React prevents you from mutating them. Let's have a look at an example (interactive example):
When you click the button, you might expect the name
prop to change to "Mozart". But it doesn't! React has made props read-only, which is a reminder that we shouldn't change props.
If we were allowed to change props, React doesn't have a way of telling that we've changed the data. Our UI is now stale - not up-to-date with the latest data - and has no way of knowing that it has to re-render.
From this we can get a clue about when to use state. If data changes over time, then we need to use state. My rule of thumb is that I always use props until I know that it needs to change over time, then I convert it to state.
Last updated