Day 3: ReactJS - State Management with useState Hooks and Event Handling

Hooks are special functions that let you "hook into" React features.

Something about connection I hear. Yes you are right. As per the name "hooks" lets you use states at ease. They provide you with Functional Components and allows developers to hook into other states without having to write extra code. In other words we can emphasize that Hooks improve the resuability in your code.

The most commonly used hooks are useState and useEffect.

useState Hooks:

In React, state refers to a built-in object that holds data or information about the component. State in a component can change over time, and when it does, the component re-renders to reflect these changes.

A comparatively simple Hook which lets you add state in Functional Components.

The useState hook is used to declare a state variable in a functional component. It returns an array with two elements:

  1. The current state value.

  2. A function to update the state.

const [state, setState] = useState(initialState);

state: The current value of the state.

setState: A function that allows you to update the state.

initialState: The initial value of the state, which can be any type (number, string, array, object, etc.).


Let us begin with an example:

To import the useState hook, place the import line atop your new file (component) which we can call as Counter.js .

import React, { useState } from 'react';

function Counter() {
  // Declare a state variable named "count" and a function "setCount" to update it.
  const [count, setCount] = useState(0);
  return (
    <div>
      <h2>Counter: {count}</h2>
      <!-- Increment the count when the button is clicked */ -->
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
export default Counter;

useState(0) initializes the count state variable with a value of 0. setCount is the function that updates the state. Every time the button is clicked, setCount increments the count by 1, and the component re-renders to display the new count. To simplify the process, lets break it down in easy way.

  • Initialization: const [count, setCount] = useState(0);

    • count is the state variable, initially set to 0.

    • setCount is the function to update count.

  • Updating State: setCount(count + 1);

    • When the button is clicked, setCount updates count by adding 1.

    • React will re-render the component, displaying the updated count.

Wondering what exactly happens in depth?

Our Goal is that we want the button to actually increase the count when it’s clicked.

  • <button onClick={() => setCount(count + 1)}>Increment</button>:

    • onClick: This is an event handler in React. It means "Do something when this button is clicked."

    • setCount(count + 1): When the button is clicked, this function increases the count by 1.

    • What happens when you click?:

      • React calls setCount with the new value (count + 1).

      • The state changes, and React re-renders the component with the updated count.

      • The new count is displayed in the <h2> tag.

Ohh yeah. To see this in action, dont forget to modify your App.js a bit (as uaual):

import React from 'react';
import Counter from './Counter';

function App() {
  return (
    <div className="App">
      <Counter />
    </div>
  );
}

export default App;

So now let us think of a way to improvise our app. How about I tell you that we can use useState hook and initialize it with the help of an object? In other words we will manage the state with an object using useState hook.

import React, { useState } from 'react';

function UserProfile() {
  // Initialize state with an object
  const [user, setUser] = useState({
    name: 'John Doe',
    age: 25,
    email: 'john.doe@example.com'
  });

  // Function to update the user's name
  const updateName = (newName) => {
    setUser(prevUser => ({
      ...prevUser, // Spread operator to retain the other properties
      name: newName // Update only the name property
    }));
  };

  return (
    <div>
      <h2>User Profile</h2>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
      <p>Email: {user.email}</p>

      {/* Example button to update the name */}
      <button onClick={() => updateName('Jane Doe')}>Change Name</button>
    </div>
  );
}

export default UserProfile;

Oh and that spread operator(...). The ... is known as the spread operator (or rest operator, depending on the context) in JavaScript. It’s a powerful tool used for working with arrays and objects in a concise and efficient way.

We will have a look at a chunk of our code just above:

  // Function to update the user's name
  const updateName = (newName) => {
    setUser(prevUser => ({
      ...prevUser, // Spread operator to retain the other properties
      name: newName // Update only the name property
    }));
  };

Wondering whats happening here? Spreading (...prevUser) means the spread operator takes all the properties from the prevUser object and copies them into a new object. We are instructing that keep the other properties in the object intact, just change the name property (in one word - Overriding only name property).

const user = {
  name: 'John Doe',
  age: 25,
  email: 'john.doe@example.com'
};
const newUser = {
  ...user,
  name: 'Jane Doe'  // Only the name is updated, other properties remain unchanged
};

console.log(newUser);
// Output: { name: 'Jane Doe', age: 25, email: 'john.doe@example.com' }

Is the above example a bit complex? Dont worry I got you.

The spread operator is usually used with Arrays. Here have a look.

const numbers = [1, 2, 3];
const newNumbers = [...numbers, 4, 5];

console.log(newNumbers);
// Output: [1, 2, 3, 4, 5]

Spreading (...numbers) copies all the elements from the numbers array into a new array. We are Adding Elements (4, 5) herein you can then add new elements to the array during the copy process.

Why there is a need of Spread Operators?

  • Immutability: In React, we often need to update state in an immutable way, meaning we don’t directly modify the existing state. The spread operator allows you to create a new object with updated properties while keeping the rest unchanged.

  • Conciseness: It’s a more concise and readable way to copy and update objects compared to using methods like Object.assign.

But But!!

The same operator when used in different context can be used to retireve the rest of the properties. I will explain.

const { name, ...rest } = user;
console.log(name); // Output: 'John Doe'
console.log(rest); // Output: { age: 25, email: 'john.doe@example.com' }

We are pulling out the name property from an object. Later the Rest Operator (...rest) collects the remaining properties (age and email) into the rest object.

Basic Difference.

  • Spread Operator: ... is used to "spread" out the properties of an object or elements of an array into a new object or array.

  • Rest Operator: ... can also be used to collect remaining properties or elements into a new object or array, depending on the context.


Okay back to the code example. In case if its long lost above, I will paste it here again.

import React, { useState } from 'react';

function UserProfile() {
  // Initialize state with an object
  const [user, setUser] = useState({
    name: 'John Doe',
    age: 25,
    email: 'john.doe@example.com'
  });

  // Function to update the user's name
  const updateName = (newName) => {
    setUser(prevUser => ({
      ...prevUser, // Spread operator to retain the other properties
      name: newName // Update only the name property
    }));
  };

  return (
    <div>
      <h2>User Profile</h2>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
      <p>Email: {user.email}</p>

      {/* Example button to update the name */}
      <button onClick={() => updateName('Jane Doe')}>Change Name</button>
    </div>
  );
}

export default UserProfile;
  • State Initialization with an Object:

    • useState({ ... }): We initialize the state with an object that contains name, age, and email.

    • const [user, setUser] = useState({ ... }):

      • user is the current state, which holds the object.

      • setUser is the function used to update the state.

  • Updating the Object in State:

    • setUser(prevUser => ({ ...prevUser, name: newName })):

      • We use the spread operator (...prevUser) to copy the existing properties of the object.

      • We then override only the name property with the new value (newName).

      • This ensures that we don’t lose the other properties (age and email) when updating the state.

  • Rendering:

    • We display the name, age, and email properties of the user object.

    • There’s also a button that, when clicked, updates the name property in the state.


Okay now let us go by one more example. In this example, we will initialize the useState hook with the help of function. Till now we have used a direct initialization and initialization with an object, so we will look another way (which is there below).

In this example imagine we want to initialize a counter that starts at a random number. We can use a function to generate this random number when the component first renders.

import React, { useState } from 'react';

function RandomCounter() {
  // Function to generate a random number
  const generateRandomNumber = () => {
    console.log('Generating a random number...');
    return Math.floor(Math.random() * 100);
  };

  // Initialize the state with the random number generator function
  const [count, setCount] = useState(generateRandomNumber);

  return (
    <div>
      <p>Initial Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

export default RandomCounter;

Here, generateRandomNumber() is a function which generates a random number between 0 to 99. Later instead of passing values which we did earlier, we directly pass this function to the useState hook. React calls this function once when the component first renders, and the returned value (a random number) is used as the initial state for count. The count state starts with the random number generated by generateRandomNumber. And as usual, the button allows you to increment the value by 1.

Lets see this in action to see what random number I get. You know what changes to make in App.js now ;)

34 it is. After clicking the Increment button, we get our desired output.

Working as expected.

Even though this example is simple, if generating the initial state were more complex, using a function would prevent that complexity from being re-executed on every render. It makes your intention clear that the initial value is derived from some logic, rather than being a static value. This approach is great for cases where the initial state value requires some calculation or fetching, ensuring that the computation happens only once.


Let's explore another example of using the previous state to update the state. This time, we'll create a simple "Like" button that toggles between "Liked" and "Not Liked" while keeping track of the total number of likes.

Imagine we have a "Like" button. Each time a user clicks it, the button toggles between "Liked" and "Not Liked." Additionally, we want to track the total number of likes, which only increases when the button is set to "Liked."

Have a look at the code:

import React, { useState } from 'react';

function LikeButton() {
  // State to track whether the button is liked or not
  const [liked, setLiked] = useState(false);

  // State to track the total number of likes
  const [totalLikes, setTotalLikes] = useState(0);

  // Function to handle the like button click
  const handleLikeClick = () => {
    setLiked(prevLiked => {
      if (!prevLiked) {
        // If the button was not liked before, increment the total likes
        setTotalLikes(prevTotal => prevTotal + 1);
      } else {
        // If the button was liked before, decrement the total likes
        setTotalLikes(prevTotal => prevTotal - 1);
      }
      // Toggle the liked state
      return !prevLiked;
    });
  };

  return (
    <div>
      <button onClick={handleLikeClick}>
        {liked ? 'Liked' : 'Not Liked'}
      </button>
      <p>Total Likes: {totalLikes}</p>
    </div>
  );
}

export default LikeButton;

We are using 2 states; one to initialize the the number of likes and the second one to track the number of likes and dislikes. In the first state we initialize the "liked" to false so as to indicate it was not "liked" initially, user will later like it and increase the like count. useState(false) suggests us that the initial state is false, meaning the button starts as "Not Liked."

While setting/tracking the number of likes, we start the number of likes with 0 (You know....sounds natural).

Next comes the function, which will handle our click around like/dislike. Wionhere we used this functsetLiked(prevLiked => { ... }). Let us explore it in detail:

  • We use a function inside setLiked to ensure that the update is based on the most recent value of liked.

  • prevLiked: This represents the current value of liked before the state update.

  • return !prevLiked: We toggle the liked state. If prevLiked was false (not liked), it becomes true (liked), and vice versa.

Let us not forget the updation part.

  • If the button was previously "Not Liked" (!prevLiked is true), we increase totalLikes by 1 using setTotalLikes(prevTotal => prevTotal + 1);.

  • If the button was previously "Liked" (prevLiked is true), we decrease totalLikes by 1 using setTotalLikes(prevTotal => prevTotal - 1);.

Lastly we simply render on the UI, the number of likes.

After hitting the button:

And this is what it is. By using a function inside setState, we can safely toggle or modify the state based on its most recent value.


Arrow Functions:

Arrow functions are a concise way to write functions in JavaScript. They were introduced in ES6 (ECMAScript 2015) and are particularly useful in React because they help maintain the context of this, making code cleaner and more predictable.

The syntax for arrow functions is different from traditional functions. Here's a simple comparison:

// A Traditional Function
function add(a, b) {
  return a + b;
}
// An Arrow Function
const add = (a, b) => {
  return a + b;
};

Additionally, If the function body only contains a single expression, you can omit the return keyword and the curly braces as below:

const add = (a, b) => a + b;

There are many things around "this" keyword when using arrow functions. Seriously!!

Arrow functions do not have their own this. Instead, they inherit this from the surrounding context (lexical this). This is one of the main reasons they are popular in React, where you often need to pass event handlers and avoid manually binding this.

class MyClass {
  constructor() {
    this.name = 'Arrow Function Example';
  }

  printNameTraditional() {
    return function() {
      console.log(this.name);  // `this` is undefined
    };
  }

  printNameArrow() {
    return () => {
      console.log(this.name);  // `this` refers to MyClass instance
    };
  }
}
const instance = new MyClass();
const traditional = instance.printNameTraditional();
const arrow = instance.printNameArrow();

traditional();  // Output: undefined
arrow();        // Output: Arrow Function Example

In the traditional function, this inside printNameTraditional doesn't refer to the instance of MyClass but instead to the global object (or undefined in strict mode). In the arrow function, this correctly refers to the MyClass instance.

We will be covering arrow functions in depth later on as we will see more code examples in further blogs. As forthe starters pack, this seems enough.


This wraps up Day 3! By now, you should have a solid understanding of how to manage state with useState hooks and handle events in React. These are crucial skills for building dynamic and interactive applications. Until next time devs!

Ciao