Crafting a state in a function component
When you visit a typical web page, it asks for your username and password. After you log in, it displays the content of what the website provides, such as blogs, tweets, or videos, in a chronological order. You can vote on them and put your comments there – a very typical web experience these days.
When you surf a website like that as a user, you don't put too much thought into how any of the actions are implemented, nor do you care about the order in which each is fired. However, when it comes to building the site yourself, each action and the time at which each gets fired starts to become important.
An action handler fires when a user clicks a button, hovers over an icon, scrolls down a paragraph, types on the keyboard, and so on. A typical relationship between a user event and an action handler is illustrated in the following:
|--x---x---x-x--x--x------> user event |--a---a---a-a--a--a------> action handler
In the preceding sketch, basically, an x
in the user event
series is followed by an a
in the user event
series. Based on this, we can start to handle a user action.
Let's turn ourselves to a "Hello World" Title
component with a button inside. Each time we click the button, a counter gets incremented by one and appended after Hello World+, as shown in Figure 2.1:
To implement that, we start with a count
variable to store a number initialized as 0
:
function Title() { let count = 0 const onClick = () => { count = count + 1 } return ( <> <button onClick={onClick}>+</button> <h1>Hello World+{count}</h1> </> ) }
In the preceding Title
component, the response to the user click is implemented via a React event handler, onClick
, wired to a button
element.
A React event handler is written slightly differently from a DOM event handler. You can tell from the onClick
camel case name, rather than the onclick
lowercase name. A React event is a synthetic event that is a cross-browser wrapper around the browser native event. In this book, we expect them to behave in exactly the same way.
Thanks to the JavaScript closure, we can directly access any component variable inside the event handler. The count
variable does not need to be passed into onClick
as a function input argument to be accessed.
If we run the code, we'd expect the title to display Hello World+1 after we click the button. But to our surprise, no matter how many times we clicked the button, it still displayed Hello World+0. To figure out what happened, let's add console.log
to two locations.
One is placed before count = count + 1
to confirm what the count
is after incrementation. Another one is placed before the return
statement to confirm what the updated count
is when the Title
component is updated. They are marked at ➀ and ➁ in the following code:
function Title() { let count = 0 const onClick = () => { console.log('clicked', count) ➀ count = count + 1 } console.log('updated', count) ➁ return ... }
With these two logs placed, we can rerun the code and generate a new timeline sketch:
|----0--1-2--3-4----5------> clicked ➀ 0--------------------------> updated ➁
From the preceding printout, a clicked
series at ➀ showed the count
number when the button was clicked, and it was clicked six times. Let's turn to another log, the updated
series at ➁; the count
value got updated once as 0
, which explains why the display remained as Hello World+0
.
The updated
series with only one printout at the very beginning indicates that there weren't any more updates after the first one. This is quite a discovery. If there were no more updates, how can we expect to see a change on the screen?
Playground – No State
Feel free to play with this example online at https://codepen.io/windmaomao/pen/jOLNXzO.
As you might already realize, we need to request a new update after the click.
Requesting a new update
To make an update, for the time being, we can borrow the render
function provided by React, as we have already used it to update the rootEl
element:
ReactDOM.render(<Title />, rootEl)
Let's take a minute to see how React updates the screen in general (see Figure 2.2). The detail involving updates can be quite complex; for now, let's treat it as a black box. We will get into more details later in the book:
When an app starts, it lands on an update. This first update is a bit special. Because all the DOM elements need to be created, we refer to this update as a mount.
What's important to know is that a new update wouldn't arrive unless it's requested, just as we invoke a render
function. When people first come to React, they might think it works as a game engine.
For instance, a game engine would request a new update every 1/60 second behind the scenes. But React does not do that! Instead, the developer should get precise control of when a new update is requested. And most of the time, the frequency is a lot lower than 1/60 second, and it's more or less driven by how fast a user acts on the website.
So with this, to bring the new count
to the screen, another update needs to be requested manually; if we borrow the render
, we can use it after the count
is incremented:
const onClick = () => { console.log('clicked', count) ➀ count = count + 1 ReactDOM.render(<Title />, rootEl) }
If we run the preceding code with the addition of render
, the timeline sketch changes to the following:
|----0--0-0--0-0----0------> clicked ➀ 0----0--0-0--0-0----0------> updated ➁
To our surprise, all numbers displayed were 0
. Looking at the updated
series at ➁
, note we got seven printouts, which means we got six more updates on top of the first update. However, the clicked
series at ➀
shows that the count
value changed to 0
and stopped to increment any more. Weird?!
How could the count
value be stuck at 0
? Something must happen to the new update, but the render
function can't be the one that resets the count
value back to 0
, can it?
It's important to know that upon the render
function being called and a function component being updated, the function that defines the component gets invoked, as shown in Figure 2.3:
With this knowledge, let's take a look at the Title
function again:
const Title = () => { let count = 0 // omitting the onClick statement console.log('updated', count) ➁ // omitting the return statement }
In the preceding code, we intentionally omit the onClick
and return
statements to make the code a bit cleaner. What was left became a let count = 0
declaration statement. During each update, the Title
function gets invoked, thus creating a new scope of the function. Inside this scope, there's a variable count
value created locally to hold a 0
number. So this code doesn't seem to do much.
It's not too difficult to see now why the count
value remains at 0
, isn't it? It doesn't really matter if we have added the increment logic onClick
or return
statement. Upon each update, the entire function scope gets a new one with a count
value declared and set to 0
. That explains why the console.log
statement followed a printed 0
.
This is actually the reason why a function component was named as a stateless function when it was introduced to React initially. "Stateless" refers to the fact that a function component can't carry or share a value to another update. In a simple word, the function reruns in each update with the same output.
Okay, now we understand the problem. So, it makes us consider saving the count
value somewhere and making it persistent for another update.
Making a value persistent
JavaScript supports a function scope: Variables defined inside a function cannot be accessed from anywhere outside the function, thus each function has its own scope. If you invoke a function multiple times, there'll be multiple scopes. But no matter how many times we invoke it, it wouldn't create a different output, such as what happened in the movie Groundhog Day.
Note
The movie Groundhog Day is a 1993 fantasy comedy film, where Phil wakes up every day to find he experiences the previous day's events repeating exactly and believes he is experiencing déjà vu.
For our count
value, we can visualize what happened with the two updates in two different scopes in Figure 2.4:
Luckily, JavaScript supports a function scope in a way that it can access all variables defined inside the scope in which it is defined. In our case, if a variable is defined outside of the Title
function, we can access this variable inside the Title
functions, as this value is shared now between multiple Title
functions.
The easiest way of sharing is to create a global variable because the global variable lives in the most outer scope of the JavaScript code, thus it can be accessed inside any function.
Note
Don't be intimidated by a global variable used in this chapter. In Chapter 3, Hooking into React, we will refine this approach and see how React defines the variable in a better location.
This way, each local count
value can set/get this global count
value, as shown in Figure 2.5:
Okay, with this new global variable idea, let's see whether we can break out of our Groundhog Day situation:
let m = undefined function _getM(initialValue) { if (m === undefined) { m = initialValue } return m } function _setM(value) { m = value ReactDOM.render(<Title />, rootEl) }
In the preceding code, a global variable, m
, is allocated, and it comes with _getM
getter and _setM
setter methods. The _getM
function returns the value but sets the initial value for the first time. The _setM
function sets the value and requests a new update. Let's apply _getM
and _setM
to our Title
component:
function Title() { let count = _getM(0) const onClick = () => { console.log('clicked', count) ➀ count = count + 1 _setM(count) } console.log('updated', count) ➁ return ... }
Inside the preceding amended Title
component, all count
variables across updates are linked with the help of _getM
and _setM
. If we rerun the code, we can see the following timeline sketch:
|----0--1-2--3-4----5------> clicked ➀ 0----1--2-3--4-5----6------> updated ➁
Wow! The screen changes to Hello World+1
upon the first click and increments further upon more clicks, as shown in Figure 2.6:
Congratulations! You just crafted a state inside a function component.
Playground – Count State
Feel free to play with this example online at https://codepen.io/windmaomao/pen/KKvPJdg.
The word "state" refers to the fact that it's persisted for all updates. For our convenience, we also change the state and request a new update afterward to reflect the change to the screen.
So, now we know how to handle a user action with a state. Let's see whether we can expand this idea further to support multiple states instead of one state.
Support multiple states
It's great that we can establish a state persistent within a function component. But we want more states like that. An app normally contains lots of buttons, switches, and actionable items; each requires a state to be persistent. So, it's a must-have to support multiple states in the same app.
So, say we need two buttons and each needs to be driven by a state. Let's extend what we have learned from a single state:
const Title = () => { let countH = _getM(0) let countW = _getM(0) const onClickH = () => { countH = countH + 1 _setM(countH) } const onClickW = () => { countW = countW + 1 _setM(countW) } return ( <> <button onClick={onClickH}>+</button> <h1>Hello+{countH}</h1> <button onClick={onClickW}>+</button> <h1>World+{countW}</h1> </> ) }
In the preceding code, we first created two buttons, one with a Hello label and one with a World label, and each have their separate event handler, onC1ickH
and onClickW
respectively. Also, we applied _getM
and _setM
to both of them, and installed a couple of logs to help the debug, as shown in the following timeline sketch:
|----0--1-2----------------> clickedH |------------3-4----5------> clickedW 0----1--2-3--4-5----6------> updatedH 0----1--2-3--4-5----6------> updatedW
From the preceding sketch, we clicked the Hello button three times and then clicked the World button three times. The numbers corresponding to both buttons all updated upon clicking, as shown in the updatedH
and updatedW
series. However, the two series seem to be inseparable and in sync, meaning clicking one button would increment both values at the same time!
Playground – Linked States
Feel free to play with this example online at https://codepen.io/windmaomao/pen/qBXWgay.
Okay, it's not too difficult to find out that we actually made a mistake by wiring the same state to both buttons; no wonder they updated at the same time:
let countH = _getM(0) let countW = _getM(0)
Although this is not what we wanted to achieve, it's interesting to see that a state is shared by two buttons. Visually, we linked two buttons; clicking one triggers the click on another.
So, what can we do if we want to have two separate states with each controlling one button? Well, we can just add another state. This time, we want to be a bit more generic in using a list to hold any number of states.
There are lots of ways to keep track of a list of values in JavaScript; one of the ways is to use a key/value pair, as in an object:
let states = {} function _getM2(initialValue, key) { if (states[key] === undefined) { states[key] = initialValue } return states[key] } function _setM2(v, key) { states[key] = v ReactDOM.render(<Title />, rootEl) }
In the preceding code, we declare a states
object to store all state values. The _getM2
and _setM2
functions are almost similar to the single-value version we crafted earlier, except this time we store each state under states[key]
instead of m
, thus a key
is needed to identify each state. With this change, let's amend the Title
component:
function Title() { let countH = _getM2(0, 'H') let countW = _getM2(0, 'W') const onClickH = () => { console.log('clickedH', countH) countH = countH + 1 _setM2(countH, 'H') } const onClickW = () => { console.log('clickedW', countW) countW = countW + 1 _setM2(countW, 'W') } console.log('updatedH', countH) console.log('updatedW', countW) return ... }
In the preceding amended version, we give a key to two states as H
and W
. We need this key for both set
and get
when a state is involved. Rerun the code and take a look at the timeline sketch:
|----0--1-2----------------> clickedH |------------0-1----2------> clickedW 0----1--2-3--3-3----3------> updatedH 0----0--0-0--1-2----3------> updatedW
Once again, we clicked the Hello button three times and World button three times in a row. The numbers on both buttons all updated upon clicking, but this time, countH
and countW
are actually incremented separately, as you can see in the updatedH
and updatedW
series.
After the first three clicks on the Hello button, countH
stays at 3
when we click on the World button. This is what we want to have, two separate states, as shown in Figure 2.7:
Playground – Multiple States
Feel free to play with this example online at https://codepen.io/windmaomao/pen/dyzbaVr.
The state we crafted so far requests a new update. This is a very good use of persistency in a function component; since being persistent is actually quite a generic feature, it should be utilized for many different purposes. So, what other things can we do with it? Let's take a look at another usage of a state.
Listen to a value change
You might wonder why we need to listen to a value change. Aren't the developers the ones who control the change of a value? As in the previous example, we use the event handler to change a counter. We know in this case exactly when the value gets changed.
That's true for this case, but there are other cases. You might send a value into a child component via a prop, or there might be two components that touch a value at the same time. In either of these cases, you can lose track of the moment when the value is changed, but you still want to perform an action upon the value change. This means that you want to have the ability to listen to a value change. Let's set up one example to demonstrate this.
Say in our Hello World button example that for any count
change, we want to know whether this value has recently been changed:
function Changed({ count }) { let flag = 'N' return <span>{flag}</span> }
In the preceding Changed
component, there's a count
prop that is sent from its parent, say any of the Hello or World buttons that we built earlier. We want to display Y
or N
, depending on whether the count
value has changed. We can use this Changed
component in the Title
component:
function Title() { ... return ( <> <button onClick={onClickH}>+</button> <h1>Hello+{countH}</h1> <Changed count={countH} /> <button onClick={onClickW}>+</button> <h1>World+{countW}</h1> </> ) }
Note that in the preceding code, we add the Changed
component between two buttons, and what we want to see is the Changed
component display Y
when we click the Hello button, and the Changed
component display N
when we click on the World button. Essentially, we want to know whether the change is coming from the Hello button or not. But when we ran the code, here's what we got in the timeline sketch:
0----1--2-3--3-3----3------> updatedH 0----0--0-0--1-2----3------> updatedW N----N--N-N--N-N----N------> Changed flag
From the preceding sketch, you can see that no matter which button is clicked, the flag
in the Changed flag
series displayed N
. This comes as no surprise, since you might have already noticed that the flag
inside the Changed
component is fixed at N
, so it wouldn't work the way we wanted. But the reason we wrote N
there is because we don't know what to write there to flip the flag
.
When the Hello button gets clicked three times, the countH
value, as in the updatedH
series, increments to 3
. Similarly, when the World button gets the next three clicks, the countW
value, as in the updatedW
series, increments to 3
. However, note that as the countW
value increments, the countH
value also gets printed out; see 3-3-3
in the updatedH
series.
This indicates that for each update, every element under the return
statement gets updated. Either countW
or countH
changes; it comes to a new update of the Title
component, thus updating all button
and h1
elements. The same applies to the Changed
component; whichever button changes, the Changed
function gets invoked. Therefore, we can't tell whether the update to the Changed
component is due to the Hello button or the World button.
If we print out the count
prop under the Changed
component, it will look the same as in the updatedH
series:
0----1--2-3--3-3----3------> count
Looking at the preceding count
value, in order to come up with the changed flag
for whether it changes from the previous value, we need to make a value persistent again – in this case, to get hold of the previous value. For example, 0
to 1
is a change, but 3
to 3
isn't.
Okay, to put this idea to work, let's borrow the state approach but this time apply it to a prev
value:
let prev function _onM(callback, value) { if (value === prev) return callback() prev = value }
In the preceding code, we allocated a prev
global variable and a _onM
utility function. The onM
function is designed to run a callback
function when the value
changes. It first checks whether the value
is equal to the prev
value. It returns if there's no change. But if there is, the callback
function is then invoked, and the current value
replaces the prev
value. Let's apply this _onM
function to the Changed
component:
function Changed({ count }) { let flag = 'N' _onM(() => { flag = 'Y' }, count) return <span>{flag}</span> }
With the preceding change, we rerun the code and take a look at the updated timeline sketch:
0----1--2-3--3-3----3------> updatedH 0----0--0-0--1-2----3------> updatedW Y----Y--Y-Y--N-N----N------> Changed flag
Interestingly enough, when we clicked the Hello button this time, it displayed Y
, and when we clicked the World button afterward, it changed to N
, as shown in Figure 2.8:
Wonderful! Also, notice the first Y
at the mount in the Changed flag
series, which is when countH
changes from undefined
to 0
. Please make a note here; we'll talk about it in the next section.
Playground – Listening to State Change
Feel free to play with this example online at https://codepen.io/windmaomao/pen/MWvgxLR.
Being able to listen to a value change is quite useful because it provides us with another way to perform tasks. Without it, we have to rely on an event handler, which is mostly driven by user actions. With _onM
, we can perform a task upon a value change, which can come out of any other process.
When listening to a value change, there exists a moment at the mount. This means that we can perform a task at the mount because of it. Let's take a look at it more closely.
Performing a task at the mount
Components mount and un-mount as things show up and disappear based on the business requirement. At the mount, it's common to want to perform a task such as initializing some variables, calculating some formulas, or fetching an API to get some resources over the internet. Let's use an API call as an example.
Say a count
value needs to be fetched from an online service called /giveMeANumber
. When this fetch returns successfully, we would like to reflect the change to the screen:
fetch('/giveMeANumber').then(res => { ReactDOM.render(<Title />, rootEl) })
The preceding code is what we'd like to do; however, we run into a technical issue right away. Though a new update can be requested, how can we send the returned data to the Title
component?
Maybe we can set up a prop on the Title
component to send it in. However, doing that would require us to change the component interface. Since we already have had states crafted to issue a new update, let's try that approach:
fetch('./giveMeANumber').then(res => { _setM(res.data) }) function Title() => { const count = _getM("") return <h1>{count}</h1> }
In the preceding code, by using _setM
after the fetch returns, we can update a state with the received res.data
and request a new update afterward. The new update invokes Title
and reads the latest count
from the state via _getM
.
Currently, we define the fetch
function parallel to the Title
component, but this is not the right location since we want to fetch only at the mount. To fix that, we can listen to the mount, as we have learned in the previous section:
_onM(() => { ... }, 0)
Using the preceding line, we can listen for a mount moment. Note that we watched a constant 0
instead of any variable. During the mount, the value that _onM
listens to changes from undefined
to 0
, but for other future updates, the value stays at 0
; therefore, the ...
callback gets invoked only once at the mount. Let's write fetch
inside this callback:
function Title() => { const count = _getM(0) _onM(() => { fetch('./giveMeANumber').then(res => { _setM(res.data) }) }, 0) console.log('u') return <h1>{count}</h1> }
If we run the preceding code, the timeline sketch should generate the following:
u-----u-------------------> log
At the mount of the Title
component, the count
state is set to be 0
initially. A fetch
function is performed right away, depicted as the first u
in the preceding updates
series. Only when fetch
returns successfully does the count
state get updated to a new value and refreshed to the screen. The new update is depicted as the second u
in the updates
series.
Playground – Task at Mount
Feel free to play with this example online at https://codepen.io/windmaomao/pen/PoKobVZ.
Between the first and the second update, that's how long it takes for the API to finish. The relationship between the API, the state, and two updates is illustrated in Figure 2.9. Essentially, after the API returns, it communicates to the shared state where the new update picks up later:
Now that we have crafted a state, and also seen how flexible a state can be used to either make a new update or listen to a value change, let's get hands-on and apply what we have learned to an app.