DEV Community

Shubham Sananse
Shubham Sananse

Posted on • Originally published at shubhs.hashnode.dev

useState() like a Pro ✨

This blog post covers all you need to know about the concept of a state and react useState hook from basics to advanced patterns. This blog assumes that you know about react fundamentals like Component, props, and jsx.

counter is the hello world of state management.

What is a state and why do we need state management in React? ⚛️

function Counter() {
  // State: a counter value
  const [counter, setCounter] = useState(0)

  // Action: code that causes an update to the state when something happens
  const increment = () => {
    setCounter(counter + 1)
  }

  // View: the UI definition
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

as you can see in this example there three main parts to Counter component

  • State is the truth that drives our application
  • View is UI based on the state
  • Action is an event that occurs in the application and changes the state

React uses the stateful values (which is generated by hook APIs like useState, useReducer) to know when to update the UI (View) part of an application. Whenever this state value changes React will update the component so that the state of the UI is the same as the state of the Component.

useState Hook 🎣

useState is a function that accepts one argument as an initial value of any type for the state, and returns 2 elements in an array, first is the state value and the second is the updater function that we use to update the state value. This returned array is usually destructured so we can name the variables whatever we want but it is good practice and a common convention to prefix the set in front of the updater function.

// you can pass any data-type
setState() // if you don't pass anything than value will be updated with undefined 
setState('Thanks') // String
setState(4) // Number
setState(['reading']) // array 
setState({ share : 💗 }) // object
setState(null) // null 
Enter fullscreen mode Exit fullscreen mode

In our Counter component 0 is the initial value for the state, and using destructuring we are naming our 2 variables counter and setCounter. setCounter is used to update the counter value by 1 every time the button is clicked.

function Counter() {
  const [counter, setCounter] = useState(0)

  const increment = () => {
    setCounter(counter + 1)
  }

  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Lazy initialization of state 🦥

Every time React re-renders the component, useState(initialState) is executed. if the initial state is some expensive function computation, e.g reading data from localStorage, mapping over some large amount of data, the instance with multiple methods ( e.g DraftJs or ThreeJs instance), then the component might face some performance issues.

// format : useState(() => initalState) 

const [token, setToken] = useState(() => window.localStorage.getItem('token') || '')

Enter fullscreen mode Exit fullscreen mode

we can use the lazy initalization to avoid the performance bottleneck for this all you need to do is put your initial state in function and that's it.

Update the state with callback 🤙

 const [counter, setCounter] = useState(0);

 const increment = () => {
    setCounter(counter + 1);
    setTimeout(() => {
      setCounter(counter + 1);
    }, 1000);
  };
Enter fullscreen mode Exit fullscreen mode

we have changed the increment function of the previous example, now we have added asynchronous behavior in our function what do you think the output will be?









Take a pause and think ,








Spoilers Ahead







You would see that after clicking the button once, even though we have 2 setCounter calls, we still get a new count updated with 1 only.

So what is actually happening? 🤔

The problem is that the second call to the setCounter is getting the same counter value as the first one. here in the example, both the setCounter got the value of counter as 0 so they updated it to 1.

But why 2nd the updater is getting the value of 0? 😕

For this, you need to understand how re-rendering actually works in React, We will not into the depths of this but in short re-rendering means if your state changes your whole component is replaced with the new one, In this example whole Counter is called again and then it gets the new value. here we are using multiple consecutive updates and due to closure setCounter has access to the counter variable one we got from array destructuring which has a value of 0.

In the example we have the initial value as 0 when the button is clicked we update the value from 0 -> 1 but to get that updated state(1) react needs to re-render the component but here we are calling the setCounter again to update counter with +1 and it gets counter as 0 so after one second when it updates the value 1.

For most async activities useEffect would be a better choice. and for more complex states useReducer.

Solution 🔥

When the new state is dependent on the previous state you can update the state with a callback.

const increment = () => {
    setCounter(counter + 1);
    setTimeout(() => {
      // callback inside a updater function
      setCounter(counter => counter + 1);
    }, 1000);
  };
Enter fullscreen mode Exit fullscreen mode

If you would change the increment function with this new one you would have a reference to the internal state and not the closure value of state.

Use-cases 💼

// toggle a boolean
const [toggled, setToggled] = useState(false);
setToggled(toggled => !toggled);

// Update an object 
const [size, setSize] = useState({ height : 500, width : 800})
setSize(currentSize => ({...currentSize , height : 700}))

// Update items in array
const [items, setItems] = useState([]);
setItems(items => [...items, 'push']);
Enter fullscreen mode Exit fullscreen mode

Top comments (0)