Hooks which were introduced in React version 16.8, provide function components access to React's state and lifecycle features. Hooks make components more organized and reusable by enabling developers to divide components into smaller parts that can be joined easily.
The most commonly used hooks which are useState
and useEffect
are useful in React apps as useState
allows developers to add state to functional components, while useEffect
helps manage side effects such as data fetching and DOM updates. Both hooks can be combined to carry out complex tasks.
Although these fundamental hooks are necessary for creating React applications, this article focuses on uncovering lesser-known React hooks that offer specialized solutions for improving performance and simplifying code.
useContext
The useContext hook is a fundamental part of React that provides a better way to manage state globally. It provides a way to share state across multiple nested components without needing to pass props through every level of the component tree.
Use case: counter app
When a state is passed down nested components without the use of the useContext
hook like in the example below, it results in what is called "prop drilling".
import React, { useState } from 'react';
function Counter () {
const [count, setCount] = useState(0);
return (
<div>
<h1>Counter</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<ParentComponent count={count}/>
</div>
);
};
function Counterchild ({ count }) {
return (
<div>
<h2>Counter Child</h2>
<ChildComponent count={count}/>
</div>
);
};
function Counterdisplay ({ count }) {
return (
<div>
<h3>Counter Display</h3>
<p>Count: {count}</p>
</div>
);
};
export default Counter;
In this example, the count
state is defined in the Counter
component, and it's passed down as props through the Counterchild
to the Counterdisplay
. Each component receives the count
state as a prop, allowing them to display it's value.
Although this method is effective, it becomes lengthy when the state is passed down through many nested child components.
To use the useContext
hook, we first import createContext
and initialize it.
import { useState, createContext } from "react";
const count = createContext()
Next, we'll wrap the tree of components that require the state with the context provider.
function Counter () {
const [count, setCount] = useState(0);
return (
<UserContext.Provider value={user}>
<div>
<h1>Counter</h1>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<Counterchild count={count}/>
</div>
</UserContext.Provider>
);
};
To make use of the previously created context located in the parent component, we need to import useContext
.
import { useContext } from "react";
Now, our context is available for use in the child component.
function Counterdisplay() {
const user = useContext(count);
return (
<div>
<h3>Counter Display</h3>
<p>Count: {count}</p>
</div>
);
}
useRef
The useRef hook allows us to create a mutable reference to a value or a DOM element. Unlike the useState
hook, useRef doesn't cause a re-render when its value changes.
useRef
accepts one argument as the initial value and returns one object called current
. For example:
const initialValue = ''
const reference = useRef(initialValue)
A reference can be accessed and changed using the current
object:
useEffect(() => {
reference.current = 'My Reference';
});
Use case 1: Tracking application re-renders
We would start an infinite loop if we attempted to count how many times our application renders using the useState
hook as it causes a re-render itself. To avoid this, we can use the useRef
Hook:
import { useState, useEffect, useRef } from "react";
function App() {
const [inputValue, setInputValue] = useState("");
const count = useRef(0);
useEffect(() => {
count.current = count.current + 1;
});
return (
<>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<h1>Render Count: {count.current}</h1>
</>
);
}
In the example above we set the initial value of the reference with useRef(0)
and access the value of count using count.current
.
Use case 2: Focusing the input element
useRef
can be used to hold a reference to a DOM element, we can then directly access and perform functions on this element by carrying out the following:
Define the reference to be used in accessing the element
Assign the reference to the
ref
attribute of the element to be interacted with
import { useRef } from "react";
function App() {
const inputElement = useRef();
const focusInput = () => {
inputElement.current.focus();
};
return (
<>
<input type="text" ref={inputElement} />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
In the example above, the reference that was created was assigned to the input
tag, and we were able to focus the element using the reference assigned to it.
Limitations
Updating a reference should be done inside a useEffect
callback or inside handlers (event handlers, timer handlers, etc.), not inside the immediate scope of the functional component's function, as the function scope of the functional component should either calculate the output or invoke hooks.
import { useRef } from "react";
function App() {
const count = useRef(0);
useEffect(() => {
count.current++; // Success
setTimeout(() => {
count.current++; // Success
}, 1000);
}, []);
count.current++; // Error
}
useReducer
The useReducer hook is used for managing complex state logic in a more organized way. It's an alternative to useState
and is particularly useful when state changes entail numerous actions.
useReducer
takes in a reducer function and an initial state. It returns the current state and a dispatch function that you can use to send actions to update the state based on the logic defined in the reducer function
Use case: Creating a counter
import { useReducer } from 'react';
// Reducer function
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
};
function Counter() {
const initialState = { count: 0 };
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
</div>
);
}
export default Counter;
In this example, the reducer function handles different actions ('INCREMENT' and 'DECREMENT') to update the state. The useReducer
hook initializes the state with { count: 0 }
and provides a dispatch function to trigger these actions.
When we click the "Increment" button, it dispatches an action of type 'INCREMENT', which causes the state to be updated and the count to increase. Similarly, clicking the "Decrement" button dispatches an action of type 'DECREMENT', reducing the count.
useCallback
The useCallback hook is primarily used to memoize functions, preventing their needless re-creation on each render. This can be very helpful when sending functions to child components as props.
Use case: Improving performance
The useCallback
's basic syntax is as follows:
import React, { useState, useCallback } from 'react';
function ChildComponent({ handleClick }) {
console.log('ChildComponent rendered');
return <button onClick={handleClick}>Click me</button>;
}
function ParentComponent() {
const [count, setCount] = useState(0);
// Using useCallback to memoize the function
const increment = useCallback(() => {
setCount(count + 1);
}, [count]);
console.log('ParentComponent rendered');
return (
<div>
<p>Count: {count}</p>
<ChildComponent handleClick={increment} />
</div>
);
}
The increment
function is memoized because the useCallback
hook ensures the function remains the same between renders if the dependencies (count
in this case) haven't changed. This prevents the unnecessary re-creation of the increment function when the ParentComponent
re-renders.
The increment function is then passed as the handleClick
prop to the ChildComponent
. Thanks to useCallback
, the handleClick
prop will remain stable between renders, even as the ParentComponent
updates its state.
useMemo
The useMemo and useCallback
hooks have some similarities. The main difference is useMemo
returns a memoized value whereas useCallback
returns a memoized function.
Use case: Improving performance
The useMemo
Hook can be used to keep expensive, resource intensive functions from needlessly running, thereby improving the performance of the React app.
import React, { useState, useMemo } from 'react';
function ParentComponent() {
const [count, setCount] = useState(0);
// Using useMemo to optimize a calculation
const squaredCount = useMemo(() => {
console.log('Calculating squaredCount');
return count * count;
}, [count]);
console.log('ParentComponent rendered');
return (
<div>
<p>Count: {count}</p>
<p>Squared Count: {squaredCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default ParentComponent;
In this example, the useMemo
hook is used to optimize the calculation of the squaredCount
value and the calculation depends on the count state. By specifying [count]
as the dependency array, we ensure that the calculation is only recomputed when the count state changes.
When you click the "Increment" button, the count state is updated, triggering a re-render of the ParentComponent
. However, thanks to the useMemo
optimization, the calculation of squaredCount
only occurs when the count state changes, avoiding unnecessary recalculations.
Conclusion
React hooks are crucial tools for front-end development for creating dynamic and effective applications. This article delves into lesser-known hooks like "useContext," "useRef," "useReducer," "useCallback," and "useMemo" that provide solutions for various challenges. I urge you to experiment with and include these hooks in your projects to attain a better understanding of React's capabilities.
Top comments (0)