Exploring React Hooks: Simplifying State and Lifecycle in Functional Components
In the React library, we have two types of components - the class components and the functional components. The difference is that class components are stateful, whereas the other ones are stateless. Well, that's what it was before React 16.8.
Every component in React goes through three phases - Mounting, Updating and Unmounting. It is known as the React Component Lifecycle. There are various methods to access these stages (see here). Only The class components had access to the state and lifecycle methods. It was not available to the functional ones. They were just simple javascript functions that took input parameters or props and returned an HTML component for rendering.
All of this changed with the introduction of Hooks in React 16.8.
What are Hooks?
The Hooks API allows the Functional components to 'hook' into their state and lifecycle phases. So now, they can do everything that was previously possible only in Class-based components.
There are only some rules that we need to follow when using hooks.
We can only call Hooks inside React function components.
We can only invoke them at the top level.
Hooks cannot be conditional.
Using Hooks requires us to write less code when compared with class-based components. They decouple the logic from the UI. At the same time, they also keep related logical units in one place. All of this allows for easy sharing of stateful logic between components. It makes the code more reusable. The community widely accepted the Hooks API for these reasons.
Let's see how we can do all this with hooks.
Managing State with Hooks
State management is a necessary part of any application. In class components, we have the state
field, which is an object containing all the state variables.
For functional components, we have the useState
hook. This hook takes the initial value for the state variable and returns an array. The first element of this array is the state variable, and the second one is its setter function, which we will use to change its value as needed. The component re-renders every time the state is updated.
import React, { useState } from 'react';
function Counter() {
let [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Looks cool, right?
The initial value can be anything from primitive data types like string
, number
, and boolean
to compound data types like Objects
and Arrays
.
To update the value, we use the setter/update function. We can either pass the new value or a function which takes the current state value as the parameter and returns the new value for the state. We use functional updates in places where the new value depends on the previous one.
//putting new value
setCount(10);
//functional update
setCount((current) => (current + 1));
A thing to note is that when using compound types as state, one must not mutate the existing value for update. Instead, they must give a new object or array.
let [state, setState] = useState({
name: "Akhilesh Pal",
job: "SDE"
});
//method 1
setState({...state, job:"Programmer"});
//method 2
setState(current => ({...current,job:"Programmer"}));
If not done like this, react will not trigger re-render. It happens due to the way JS handles data.
Performing side effects during the Lifecycle Phases
A react component goes through the following three phases in its lifecycle - mounting, updating and unmounting. The Mounting Phase begins when a component is created and inserted into the DOM. The Updating Phase occurs when a component's state or props change. And the Unmounting Phase occurs when React removes it from the DOM. Often, we perform some actions when these events happen, like API calling or some calculations.
For Class Components, we have various methods like componentDidMount()
, componentDidUpdate()
, componentWillUnmount()
and many more to access these events. In Functional Components, we have the useEffect
hook. It is a hybrid that can do the combined task of the three methods mentioned previously.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
The useEffect()
hook takes an effect function and a dependency array as an argument. This effect function executes when the component mounts and on further updates based on the dependency array. The dependency array takes a set of state variables that it tracks for updates. If any of their value changes, then it executes.
/* If we don't provide a dependency array, the effect will run on every re-render */
useEffect(() => {
console.log("Effect running on every re-render");
});
/* If we provide an empty dependency array, the effect will run only on the mount phase */
useEffect(() => {
console.log("Effect running only on mount phase");
}, []);
/* If we provide a dependency array with some variables, the effect will run on re-render if their value is updated */
const [count, setCount] = useState(0);
useEffect(() => {
console.log("Effect running on re-render if count is updated");
}, [count]);
So, useEffect
takes care of the Mount and the Update phase, but what about Unmount? Well, it handles that as well. What we do is return a cleanup function from the effect function. This function executes before every re-render and upon Unmount.
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// Cleanup function to clear the timer when the component unmounts
return () => {
clearInterval(timer);
};
}, []);
return (
<div>
<p>Count: {count}</p>
</div>
);
}
export default Example;
Referencing
Sometimes, we want data to persist upon re-render but not cause a re-render when its value is updated. For example, let's say there is an interval in our project that we stop at some event. We need to store its 'id'. This 'id' is not going to be rendered. Hence, this is a perfect use case for references.
For creating references, we have the useRef
hook. It returns a reference object. This object has a current
property that holds the current value for the reference. We can pass the initial value for the reference as an argument to the hook.
import React, { useRef, useState } from 'react';
function Counter() {
const intervalRef = useRef(null);
const [count, setCount] = useState(0);
const startTimer = () => {
// Start a timer that increments count every second
intervalRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
};
const stopTimer = () => {
// Stop the timer when the button is clicked
clearInterval(intervalRef.current);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={startTimer}>Start Timer</button>
<button onClick={stopTimer}>Stop Timer</button>
</div>
);
}
export default Counter;
We also use Refs to reference DOM elements. Sometimes, we might need access to the DOM elements managed by React—for example, to focus on a node, scroll to it, or measure its size and position. One way to do this could be to use a JS query selector. But, the correct way to do this in React is to use refs.
import React, { useRef } from 'react';
function TextInputWithFocusButton() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
}
Improving Performance with Caching
A React component re-renders multiple times across its lifecycle. All its variables and functions are redeclared and refined on every re-render. It might not be an issue for simple variables as the older ones will get garbage collected. But let's say there is some variable whose value gets assigned after an expensive computation. This computation happens each time the component re-renders, affecting performance. It will be better if we cache this value and re-evaluate only when the parameters for calculation update.
We use the useMemo
hook for this exact purpose. It takes the compute function and the dependency array. It memoizes the result and recalculates only when something from the dependency array updates. Its syntax is similar to that of useEffect
.
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ number }) {
const expensiveComputation = useMemo(() => {
// Simulating an expensive computation
let result = 0;
for (let i = 0; i < number; i++) {
result += i;
}
return result;
}, [number]);
return <div>{expensiveComputation}</div>;
}
The useCallback
hook does the same thing but for functions. One of its uses is memoizing event handlers. Otherwise, they will get refined on every re-render.
import React, { useState, useCallback } from 'react';
function Button({ onClick, children }) {
return <button onClick={onClick}>{children}</button>;
}
function App() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<Button onClick={increment}>Increment</Button>
</div>
);
}
Custom Hooks
There are situations when we use similar hooks or a set of hooks in different components. As you can see, this will cause a lot of repetition. It goes against code reusability. We can solve this by clubbing all these hooks into a custom hook. All we have to do is create a function whose name starts with use
. For example, useCustomHook
. Within this, we call all the hooks. Then, we use this hook instead.
import React, { useState, useEffect } from 'react';
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
return () => {
document.title = 'React App'; // Reset title on unmount
};
}, [title]);
}
function Counter() {
const [count, setCount] = useState(0);
useDocumentTitle(`You clicked ${count} times`);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
That's all Folks
That's a wrap for now, folks! I hope you enjoyed diving into this blog as much as I enjoyed sharing it with you! But it doesn't end here – your insights and feedback mean the world to me! Let's keep this conversation going in the comments below!
And hey, if you want to connect beyond these pages, catch me on Twitter! My handle is @AnshumanMahato_.
Until next time, stay curious and keep exploring! 🌟Thank You for reading this far. 😊