- Explain what hooks do and how they let us use function components instead of class components.
- Work with tuples.
- Practice converting stateful class components to functional components with the useState hook.
In this section, we'll cover the simplest hook, useState
, and how it can be used to do the same things as a Class component's this.setState
.
To get started, we'll dive into the counter example from the main Readme, going through it line-by-line.
function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(count + 1)
const decrement = () => setCount(count - 1)
return (
<div>
<span>Current Count: {count}</span>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
)
}
Tuples are basically just immutable (unchangeable) arrays. It's a concept borrowed from other programming languages like Python. We can expect an item in a Tuple to always be in that same specific index position in the array.
const [count, setCount] = React.useState(0)
The first thing we notice here is the const [count, setCount] = ...
. If this syntax is unfamiliar, we are simply using ES6 Array Destructuring. useState
returns an array with two items: the first is the value of the state, and the second is a function you can use to update that state, triggering a re-render. The above could be rewritten as:
const countState = React.useState(0)
const count = countState[0]
const setCount = countState[1]
Typically, we use arrays for storing a list of values. We often don't know what the length of that array will be. In this case, we know that useState
will always return an array of two items, the first being the state, and the second being the updating function.
When an array is used this way, it is known as a tuple
. A tuple is an array of a fixed length, with each entry in the array representing a particular value. Tuples are not often used within the JS community, but React Hooks have introduced them, and you'll likely be seeing them used in more and more libraries moving forward. The important thing to remember that a tuple is just an array.
Here's an example of an address as both an object and as a tuple:
const addressObject = {
line1: '123 Sesame Street',
line2: 'Unit ABC',
city: 'New York',
state: 'NY',
zip: '10001'
}
const addressTuple = [
'123 Sesame Street',
'Unit ABC',
'New York',
'NY',
'10001'
]
Often, it makes more sense to use an object to represent data, because the keys describe what that value is. With Tuples, developers need to know & remember what each entry in the tuple is. For example, would you rather come across address[3]
or address.state
when reading through code? Often, objects make the most sense. But, in combination with ES6 Array Destructuring, tuples have one advantage - we can easily assign each value to a variable of whatever name we choose.
For our address example, a tuple might be handy if we are working with various APIs that use different variable names. Maybe we are submitting the address to an api that uses all-uppercase properties - in that case, we could do something like this:
const [LINE_1, LINE_2, CITY, STATE, ZIP] = address
api.updateAddress({ LINE_1, LINE_2, CITY, STATE, ZIP })
When working with useState
, this means we can assign the state and and our updater function to whatever we choose:
const [username, setUsername] = React.useState('joseph')
const [zipCode, setZipCode] = React.useState(90065)
Back to this line:
const [count, setCount] = React.useState(0)
We see that we call React.useState
with a value of 0
. This argument is the initial state - in this example, count
will be assigned to 0
.
Moving on to the next two lines:
const increment = () => setCount(count + 1)
const decrement = () => setCount(count - 1)
Here, we are creating two new functions that will call setCount
. These functions are then assigned to the buttons as handlers:
return (
<div>
<span>Current Count: ${count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
When we click the +
button, the increment
function is called, which executes setCount(count + 1)
. When setCount
has been called, the component will re-render, and our count
will have been increased by one:
const [count, setCount] = React.useState(0) /* same as before, except useState will return an updated count */
So, when an update function is called, the new state will be whatever was supplied to that function. This sounds pretty straightforward (and it is!) - but this works a little differently from Class components. Keep this in the back of your mind during the exercise, and we'll cover it in the next section.
- Open up a new Codesandbox, and replace the default App component with the Counter above. (Type it out by hand!)
- Play with the initial state
- (bonus) Prevent the count from going below 0 or above 10.
- (bonus) Disable the
+
button when the count is 10, and the-
button when the count is 0. (hint: you can disable a button HTML element with thedisabled
prop:<button disabled={true}>...</button>
)
Ok, let's beef up our Counter component. In addition to tracking the count
, we want to add a controlled text input, and keep track of that value in the state.
💡 This is an example of one of the key ways that Hooks work differently from Class components
In a class component, we could do this:
class Counter extends React.Component {
state = {
count: 0
inputValue: ''
}
increment = () => {
this.setState({
count: this.state.count + 1
})
}
decrement = () => {
this.setState({
count: this.state.count + 1
})
}
onInputChange = (e) => {
this.setState({
inputValue: e.target.value
})
}
render() {
return (
<div>
<span>Current Count: ${this.state.count}</span>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<input value={this.state.inputValue} />
</div>
)
}
}
Let's take a quick look at one of these handlers:
this.setState({
inputValue: e.target.value
})
We set the state with an updated inputValue
. The component re-renders, and, assuming we hadn't clicked anything our new state is:
{
count: 0,
inputValue: 'a'
}
Notice that we didn't include the count
in our call to this.setState
. But, it was included in the next render. This is because a class component's setState
performs a shallow merge, updating only the properties of the state
object that are included in the argument.
React hooks do this differently: The updated state is not merged, but replaced completely. So, if we were to rewrite this class component like this:
function Counter() {
const [counterState, setCounterState] = React.useState({
count: 0,
inputValue: ''
})
const increment = () => setCounterState({ count: counterState.count + 1 })
const decrement = () => setCounterState({ count: counterState.count - 1 })
const handleChange = (e) => setCounterState({ inputValue: e.target.value })
return (
<div>
<span>Current Count: ${counterState.count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<input onChange={handleChange} value={counterState.inputValue} />
</div>
)
}
When we call the increment
function, our component will re-render, and our counterState
will now look like:
{
count: 1
}
The inputValue
is no longer on the state! You could merge in the existing state each time you call setCounterState
, like so:
const increment = () => setCounterState({
...counterState, // spread in the existing state
count: counterState.count + 1 // update the count, overwriting the existing count
})
But, this will quickly become cumbersome as we add more pieces of state. Later, we'll use the useReducer
hook to handle more complicated state. But in this case, we can simply call useState
twice: once for our count, and once for our input value:
function Counter() {
const [count, setCount] = React.useState(0)
const [inputValue, setInputValue] = React.useState('')
const increment = () => setCount(count + 1)
const decrement = () => setCount(count - 1)
const handleChange = (e) => setInputValue(e.target.value)
return (
<div>
<span>Current Count: ${count}</span>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
<input onChange={handleChange} value={inputValue} />
</div>
)
}
Much simpler! 👏
We can call useState
as many times as we need.
- Back in your Codesandbox, add a controlled input that is handled by its own
useState
That's it for useState
! It's super simple, and great for tracking basic pieces of your state. It's a little different from this.setState
, but can be used to do the same things.
thanks so much for this article . i never understand useState but your article make it possible for me to understand useState . thank you so much ...