Photo by Kasia Derenda on Unsplash
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:
The current state value.
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 to0
.setCount
is the function to updatecount
.
Updating State:
setCount(count + 1);
When the button is clicked,
setCount
updatescount
by adding1
.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 thecount
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 containsname
,age
, andemail
.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
andemail
) when updating the state.
Rendering:
We display the
name
,age
, andemail
properties of theuser
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 ofliked
.prevLiked
: This represents the current value ofliked
before the state update.return !prevLiked
: We toggle theliked
state. IfprevLiked
wasfalse
(not liked), it becomestrue
(liked), and vice versa.
Let us not forget the updation part.
If the button was previously "Not Liked" (
!prevLiked
istrue
), we increasetotalLikes
by 1 usingsetTotalLikes(prevTotal => prevTotal + 1);
.If the button was previously "Liked" (
prevLiked
istrue
), we decreasetotalLikes
by 1 usingsetTotalLikes(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