Diving deep into useState()

Diving deep into useState()

Discovering every aspect of useState from passing numbers to arrays to objects as arguments, to analyzing and correcting common mistakes

Hello developers,
Hope you all are doing well and building beautiful and fun applications. Today was a heck of a Sunday. This blog was supposed to be started well before, but something caught me for the whole day. I was working out a small project to display different cases of useState and something crazy stuff was happening, all thanks to STRICT MODE. But this made me realize that this mistake might be made by many developers out there and some of them might not be able to solve or know what, how and why the error happened. This pressed the urgency of this blog to be published. Below I will try to explain each and every use case of useState, different ways to making the same thing work out, some errors I got into while working, why they happened and how to solve them.

Who this blog is for ?

  • For people starting with hooks
  • Who don't know what useState is
  • Who don't know how to set array or object as state and manage it
  • Who ran into some error but don't know why or how it happened
  • Who don't know what STRICT MODE is and how it can affect useState() hook

If you are any of this, then be sure to read below.
Stay tuned.
Hope you have a wonderful reading.

What is useState hook ?

It is one of the many hooks react offers us and also the most famous and used one.
In class based components in react, we use this.state and this.setState() to initialize and manage state.
In function based components we have a special function to manage state variables, famously called as a hook - useState().

Basic syntax

import { useState } from "react";
const Counter = () => {
  const [number, setNumber] = useState(0);
  return (
    <div className="counter">
      {number}
    </div>
  );
};
export default Counter;

What argument does useState hook take ? It takes an initial state as an argument. It can be of any datatype - a numeric, an array, an object, etc.

What does it give in return ? It returns an array of 2 elements - [state, setState]. The first element is the current state and the second element is a function to update the state. You can name the variables whatever you want. But by convention the second element is named as setting prefix 'set' to the first element.

Number as a state

Let's explore how we can manage a number state in react using useState. This is the very basic. After this we will explore more advanced stuff. But first BASIC. Below is a small code implementing the same.

Code

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [number, setNumber] = useState(0);

  const increase = () => {
    // Method 1
    // setNumber(number+1);

    // Method 2
    setNumber((prevStateValue) => prevStateValue + 1);
  };

  const decrease = () => {
    // Method 1
    // setNumber(number-1);

    // Method 2
    setNumber((prevStateValue) => prevStateValue - 1);
  };

  return (
    <div className="App">
      <h1>useState() Hook</h1>
      <div className="numberStateContainer">
        <p>Maintaining number state</p>
        <div>
          <p className="number">{number}</p>
          <button className="minus" onClick={decrease}>-</button>
          <button className="plus" onClick={increase}>+</button>
        </div>
      </div>
    </div>
  );
}

Setting initial state - const [number, setNumber] = useState(0). Just pass any number as argument to useState which you want to be the initial state value.

Updating state value - There are 2 ways in which you can update the state :-

  1. setting a value directly setNumber(1)
  2. using previous state value to set new state - setNumber((prev) => prev + 1); OR setNumber(number+1);

I won't be explaining the code because I guess it is pretty much straight forward. We have 2 buttons - plus and minus. When plus is clicked increase() function will be invoked and state value will be increased by 1. When minus is clicked decrease() function will be invoked and state value will be decreased by 1.

All good!! Let's see the output.

Output

Untitled design (3).gif

Array as state

There are many times when we need to maintain an array state, storing multiple items and rendering them onto the screen. But working with arrays can be a little tricky. When you get a little experienced in the field, you just work a single way that works for you. But when you start NEW or try new ways to maintain an array as a state, it might get a little extra tricky, where sometimes you might spend a whole day trying to figure out some error. If you are a regular developer then this might not interest you much but if you are an amateur or even a developer who got curious and thought of trying out different ways instead of using the spread operator ..., this one section is for you. I'll share different ways, why doesn't a certain way work as it should, why something works perfectly in normal mode but not in strict mode and what is the best way to work with array states. Everything.
Just sit tight because it is going to be a bumpy ride.


Setting initial state - const [array, setArray] = useState([]);. Just pass an empty array as argument. If you want you can pass an array with elements as well, a 2D array. Anything you like.

Updating state value - This needs a discussion. Updating can be easily done using a spread operator - setArray((prev) => [...prev, newItem]);. What we do here is we spread out OR take out and put in all items from previous state array and also add a new item to a NEW array and set that as the new state value. This works perfectly fine but adding items might not be the thing you always need to do. We might need to use other ways of manipulating arrays. So let's discover some wrong ways and some right ways to do it below.

Basic #1

This one thing which I am going to tell you might be really very basic but might cause errors sometimes as well.
setArray(argument) - The argument which we pass needs to be a new value to cause re-rendering of the component. If we don't pass a new value, the component doesn't re-render. And this is exactly what is expected.
This info might not be very valuable when dealing with number state BUT when you are dealing with Array.....AHHHH.....I really can't say the same here.

Failed Case 1

Okay! So push() is the method we generally use to add an item to array. Let's see a small code implementing it.

const updateArray = (newItem) => {
    setArray((prev) => {
      prev.push(newItem);
      console.log(prev);
      return prev;
    });
  }

So, What do you think about the code above? Will it work? Will it serve it's purpose? Will it add a new value to the array and update the state?
A. YES
B. NO

ANSWER - B. NO
Try to run the code yourself to check out.

Explanation - Firstly focus on the Basic #1 I mentioned before. It states that "When updating a state, to update or to cause re-rendering, you need to pass a new value as argument to setState() function."
But we are returning new value, aren't we? We pushed a new item. We changed the array. But then what's going wrong?

What's going wrong here is that arrays are not compared by value but reference. Basically the reference/address of the prev array remains the same before and after adding the item. So we are returning the same thing which we received, BACK or we are returning the same previous state value instead of a new state.
Therefore, technically, the state value didn't change because the state value is not the array but a reference to the array. We need to change the reference that means we need to return a new array present at a new location to update the state value. I hope I make sense.

Failed Case 2

Previously what we were doing wrong was that we were returning the same variable back, right. So, now I am returning a new one. I am creating a new array from prev array and returning the new one. Below is the code.

const updateArray = (newItem) => {
    setArray((prev) => {
      const newArray = prev;
      newArray.push(newItem);
      console.log(newArray);
      return newArray;
    });
  }

So, What do you think about this one? Will it work?
A. YES
B. NO

ANSWER - B. NO
Try to run the code yourself to check out.

Explanation - Again, wrong. This time what we are doing wrong is the way we are copying data. If one doesn't know about copy by value and copy by reference in JavaScript, he might never be able to figure out what's wrong with the above code. What we are doing here is copying by reference. Both newArray variable and prev variable point to the same location in memory. When we push item to newArray, both variables will get affected because both are pointing to the same location. When we return newArray, we are basically returning the same address just with a different name this time. So, yes it won't work.

Failed Case 3

Here we will see how STRICT MODE is affecting our state.

  const updateArray = (newItem) => {
    setArray((prev) => {
      const newArray = prev;
      newArray.push(newItem);
      return [...newArray];
    });
  };

The code above is same as the Failed Case 2 except for the return statement. Here instead of returning the newArray we are spreading it's items into a new array and returning it. This solves the issue of passing back the same reference and state not updating. Here the state will update.

But....... There is still an issue. Let's find out

Below is a small clip of how the thing is working.

With Strict Mode.gif

Here as you can see, the code is working perfectly fine when in non strict mode - adding one item at a time, but in strict mode - It is adding 1 item the first time and same item 2 items everytime after that.
Why is that? Ans: StrictMode

Strict Mode
Strict mode is a way of making you use the best practices and prevent any wrong silent behavior. It is a tool for highlighting potential problems in an application. And this is the very thing responsible for the bad behavior above.
Strict Mode makes the callback function passed in setState() run twice before returning. The callback function runs twice and then the setState() function ends, new State is updated and the component re-renders.

In the above code, we are pushing item into newArray which points to the same location that prev points to in memory. Even if we are returning an array at a new location which enforces re-rendering, we are still updating the prev variable. Now you need to understand that the callback function runs twice before setting the new state value. Let's understand what's happening below.

  1. state = []
    prev = []
    newArray = prev
    newArray.push("Wand")
    newArray = ["Wand"]

    Now as newArray points to the same location as prev, prev = ["Wand"]
    When we change the state value for the first time, the callback runs only once.

    After first click - state = ["Wand"]

  2. state = ["Wand"]

    Callback function Run 1
    prev = ["Wand"]
    newArray = prev
    newArray.push("Wand")
    newArray = ["Wand", "Wand"]

    As location of newArray and prev is same, prev = ["Wand", "Wand"]

    After 1st run - state = ["Wand", "Wand"]

    Callback function Run 2
    prev = ["Wand", "Wand"]
    newArray = prev
    newArray.push("Wand")
    newArray = ["Wand", "Wand", "Wand"]

    Again, as location of newArray and prev is same, prev = ["Wand", "Wand", "Wand"]

    After 2nd run - state = ["Wand", "Wand", "Wand"]

    After second click - state = ["Wand", "Wand", "Wand"]

  3. Similarly, after third click - state = ["Wand", "Wand", "Wand", "Wand", "Wand"]

And it goes on.....

So, why did all this happen? All because we somehow changed the prev variable, knowingly or unknowingly.

What should you not do? You should never mutate the previous state variable.
What should you do? You should always create a new variable and set state to it.

Enough of the Failed Cases. Let's see how to do it correctly now.

How to ADD elements in array state

Way 1

const addItem = (item) => {
    setArray((prev) => {
      const newArray = [...prev];
      newArray.push(item);
      return newArray;
    });
  };

First spread the elements of prev state into new array and set it to a variable. Then add new item using push method to new variable. Return the new variable.

Way 2

  const addItem = (item) => {
    setArray((prev) => [...prev, item]);
  };

This is the most easy and shortest way.

How to DELETE an element in array state

Now I wrote a way of deleting an element from array. I really don't like searching array, getting index of the element and then deleting it.
So, let's explore my way. I don't know it it is standard or if it is the same way other developers do. But it works and that's what matters.

  const deleteItem = (item) => {
    setArray((prev) => {
      let isDeleted = false;
      const newArray = prev.filter((e) => {
        if (!isDeleted && e === item) {
          isDeleted = true;
          return false;
        }
        return true;
      });
      console.log(newArray);
      return newArray;
    });
  };

Just use a flag and filter. filter method returns a new array with elements for which the callback returns a true. So we return false when the flag is false and the element is the item which needs to be deleted and also set the flag to true because we don't want anymore items to be deleted. Else we return true.

DONE! We deleted an element successfully.

Now it's enough with the arrays. Let's move on to objects now.

Object as State

Working with an object is not very hard. It's very easy compared to arrays. Let's see how to work with them below.

Setting initial state - const [object, setObject] = useState({});. Just pass an emptyObject as argument. If you want you can pass an argument with key-value pairs as well. It's your call.

Updating state value - Updating an object can be in 3 ways - adding a key-value pair, removing one, or updating one.

First let's see how to add a key-value pair.

Adding a key-value pair

setObject((prev) => {
    const newObject = {...prev}
    newObject[keyName] = value;
    return newObject;
});

So in the above code we firstly create a new object variable from prev, having same values just different address in memory. We then add a new key-value pair and then we return the new object variable.

Note : All the failed cases apply to object as well. Don't change the prev variable and take care of strict mode.

Updating a key-value pair

To update a key-value pair, we don't have to do much. Just ADD, same as in the code in "Adding a key-value pair" section. An object cannot have multiple occurrences of same key or in normal language "An object cannot have repeated keys. Every key is unique and so is present only once."

So to update, just add a new one and if it's keyName matches a keyName already present in the object, it will replace it. The code is same as the Adding a key-value pair one.

Deleting a key-value pair

Now this is something different. Deletion is also a very easy task. Just do delete(newObject[keyName]) and you are done. Below is a small code implementing it.

setObject((prev) => {
    const newObject = {...prev}
    delete(newObject[keyName])
    return newObject;
});

Done! So now all the operations that we can perform are completed. Hurray!!!!

The End

It is already a very long blog, I know. If you came down till here, thanks for your wonderful time and effort. I hope you found it interesting and informative. Majorly I wanted to cover all the wrong ways one can go while working with state arrays and the explanation behind it because I myself went all these ways and it took me a lot of effort to understand what was wrong and all the logic and concepts behind it. So, I thought why not write it down and share it with others so I can save them from finding explanation elsewhere. I hope it was clear enough.

I tried to make it clear enough.

Here is a codesandbox embed for you where I have created a small application implementing the useState hook in different ways. Have a Good Day!!