Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Designing React Hooks the Right Way

You're reading from   Designing React Hooks the Right Way Explore design techniques and solutions to debunk the myths about adopting states using React Hooks

Arrow left icon
Product type Paperback
Published in Jan 2022
Publisher Packt
ISBN-13 9781803235950
Length 278 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Fang Jin Fang Jin
Author Profile Icon Fang Jin
Fang Jin
Arrow right icon
View More author details
Toc

Table of Contents (12) Chapters Close

Preface 1. Chapter 1: Introducing the Function Component 2. Chapter 2: Crafting States in Functions FREE CHAPTER 3. Chapter 3: Hooking into React 4. Chapter 4: Use State to Jumpstart Components 5. Chapter 5: Use Effect to Handle Side Effects 6. Chapter 6: Use Memo to Boost Performance 7. Chapter 7: Use Context to Cover an Area 8. Chapter 8: Use Ref to Hide Stuff 9. Chapter 9: Use Custom Hooks to Reuse Logic 10. Chapter 10: Building a Website with React 11. Other Books You May Enjoy

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:

Figure 2.1 – Hello World with no state

Figure 2.1 – Hello World with no state

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:

Figure 2.2 – React update

Figure 2.2 – React update

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:

Figure 2.3 – React render for a function component

Figure 2.3 – React render for a function component

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:

Figure 2.4 – Two function scopes for the two updates

Figure 2.4 – Two function scopes for the two updates

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:

Figure 2.5 – A shared value among the two updates

Figure 2.5 – A shared value among the two updates

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:

Figure 2.6 – Hello World counter using a state

Figure 2.6 – Hello World counter using a state

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:

Figure 2.7 – Hello and World buttons with two states

Figure 2.7 – Hello and World buttons with two states

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:

Figure 2.8 – Listen to value change

Figure 2.8 – Listen to value change

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:

Figure 2.9 – Fetch API within the stateful component

Figure 2.9 – Fetch API within the stateful component

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.

You have been reading a chapter from
Designing React Hooks the Right Way
Published in: Jan 2022
Publisher: Packt
ISBN-13: 9781803235950
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime