Skip to content

Instantly share code, notes, and snippets.

@iscott
Created March 24, 2020 17:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iscott/14da8b027cb1f1f3469c9ac3575b21de to your computer and use it in GitHub Desktop.
Save iscott/14da8b027cb1f1f3469c9ac3575b21de to your computer and use it in GitHub Desktop.
useState hook in React

Hooks: useState

Learning Objectives:

  • 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.

🔗Codesandbox Demo

Intro:

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

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)

Initial State

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.

Calling the update function

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.

🏋️‍♂️Exercise 1: Clicker Hook

  1. Open up a new Codesandbox, and replace the default App component with the Counter above. (Type it out by hand!)
  2. Play with the initial state
  3. (bonus) Prevent the count from going below 0 or above 10.
  4. (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 the disabled prop: <button disabled={true}>...</button>)

Multiple Hooks & Multiple pieces of state

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.

Exercise 2: More State

  1. Back in your Codesandbox, add a controlled input that is handled by its own useState

Summary

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.

@hussainali003
Copy link

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 ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment