React Hooks: Exploring the Versatility
React applications become more complex as they grow, especially when managing state, side effects, and performance. What starts as a clean codebase can quickly lead to unnecessary re-renders, tangled data flows, and components that are difficult to debug. This is where React Hooks play an important role, offering a simpler, more efficient way to build scalable, maintainable applications.
Introduced in React 16.8, Hooks were designed to simplify state management and reduce repetitive code. Since then, developers have gained a cleaner, more predictable way to tackle coding challenges. In this blog, we will explore what React Hooks are, their types, and examples that demonstrate their usage, along with some best practices for leveraging Hooks effectively.
What are Hooks in React.js?
React Hooks enable functional components to leverage state and other React features, such as lifecycle methods, without transforming them into class components. We can easily separate the reusable section of a functional component using React Hooks, which are simple JavaScript functions. Moreover, stateful hooks can manage side effects.
Programmers can manage state, handle side effects, consume context, and access the DOM using React’s built-in hooks, which include useState, useEffect, useContext, and useRef. In addition to designing hooks, developers can encapsulate and communicate their own stateful logic across multiple components.
Developers can now build cleaner, more reusable code using hooks by dividing logic into smaller, more focused functions. Using multiple hooks within a single component simplifies reasoning about the component’s state and behavior.
Types of Hooks in React
React provides a variety of built-in Hooks, each designed to solve specific problems. Some manage state, others handle side effects, while a few focus on performance optimization. Understanding these hooks will help you choose the right one based on your use case and write cleaner, more organized code. Following is the most widely used React Hooks list. Let’s look into the types in detail:
1. useState
useState is the most basic React Hook. It allows functional components to declare and manage local state. It returns an array containing the current state value and a function to update it.
Syntax:
| const [state, setState] = useState(initialValue); |
Example:
| import React, { useState } from ‘react’; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } |
Output:
A counter that displays the number of times the button has been clicked. Each click increments the count by one and re-renders the component with the updated value.
2. useEffect
useEffect lets you perform side effects in functional components. Side effects include data fetching, directly manipulating the DOM, setting up subscriptions, timers, or logging. By default, useEffect runs after every render, but you can control when it runs by providing a dependency array.
Syntax:
| useEffect(() => { // side effect logic return () => { // cleanup logic (optional) };}, [dependencies]); |
Example:
| import React, { useState, useEffect } from ‘react’; function DocumentTitleUpdater() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment </button> </div> );} |
Output:
Every time the button is clicked, the browser tab title updates to show the current count. The effect runs only when the count changes, thanks to the dependency array.
3. useContext
useContext is another type of React Hook. It provides a way to access data stored in a React Context without passing props manually through every level of the component tree. It simplifies global state sharing across deeply nested components.
Syntax:
| const value = useContext(MyContext); |
Example:
| import React, { createContext, useContext } from ‘react’; const ThemeContext = createContext(‘light’); function ThemedButton() { const theme = useContext(ThemeContext); return <button className={theme}>Current Theme: {theme}</button>;} function App() { return ( <ThemeContext.Provider value=”dark”> <ThemedButton /> </ThemeContext.Provider> );} |
Output: A button that displays “Current Theme: dark”. The ThemedButton component accesses the theme value directly from context, so it doesn’t need to be passed as a prop.
4. useCallback
useCallback returns a memoized version of a function that is recomputed only when one of its dependencies changes. It is used to optimize performance by preventing unnecessary re-creations of functions that are passed to child components, which can cause unwanted re-renders.
Syntax:
| const memoizedCallback = useCallback(() => { // function logic}, [dependencies]); |
Example:
| import React, { useState, useCallback } from ‘react’; function Child({ onClick }) { console.log(‘Child rendered’); return <button onClick={onClick}>Click me</button>;} function Parent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { console.log(‘Button clicked’); }, []); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> <Child onClick={handleClick} /> </div> );} |
Output:
Clicking the increment button updates the count but does not re-render the Child component because handleClick remains the same reference across renders. Without useCallback, Child would re-render every time the parent re-renders.
5. useMemo
useMemo is also a popular React Hook. It returns a memoized value. It recalculates the value only when one of its dependencies changes. This helps optimize expensive computations by preventing them from running on every render.
Syntax:
| const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); |
Example:
| import React, { useState, useMemo } from ‘react’; function ExpensiveCalculation({ num }) { const squared = useMemo(() => { console.log(‘Calculating square…’); return num * num; }, [num]); return <p>Square: {squared}</p>;} function App() { const [count, setCount] = useState(0); const [number, setNumber] = useState(5); return ( <div> <ExpensiveCalculation num={number} /> <button onClick={() => setNumber(number + 1)}>Change Number</button> <button onClick={() => setCount(count + 1)}>Re-render (Count: {count})</button> </div> );} |
Output: ‘Calculating square…’ is logged only when the number changes. Clicking the ‘Re-render’ button increments the count but does not trigger the expensive calculation again, because num remains unchanged.
6. useReducer
useReducer is an alternative to useState for managing complex state logic. It is particularly useful when the next state depends on the previous state or when you have multiple sub-values. It follows the reducer pattern commonly used in Redux.
Syntax:
| const [state, dispatch] = useReducer(reducer, initialState); |
Example:
| import React, { useReducer } from ‘react’; const initialState = { count: 0 }; function reducer(state, action) { switch (action.type) { case ‘increment’: return { count: state.count + 1 }; case ‘decrement’: return { count: state.count – 1 }; case ‘reset’: return { count: 0 }; default: throw new Error(‘Unknown action’); }} function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <div> <p>Count: {state.count}</p> <button onClick={() => dispatch({ type: ‘increment’ })}>+</button> <button onClick={() => dispatch({ type: ‘decrement’ })}>-</button> <button onClick={() => dispatch({ type: ‘reset’ })}>Reset</button> </div> );} |
Output: A counter with three buttons. The count increments, decrements, or resets based on the action dispatched. The reducer centralizes state update logic, making it easier to manage and test.
7. useRef
useRef creates a mutable object that persists for the entire lifetime of the component. It does not cause re-renders when its value changes. Common use cases include accessing DOM elements directly and storing values that need to persist between renders without triggering updates.
Syntax:
| const refContainer = useRef(initialValue); |
Example:
| import React, { useRef, useEffect } from ‘react’; function TextInputWithFocus() { const inputRef = useRef(null); useEffect(() => { inputRef.current.focus(); }, []); return <input ref={inputRef} type=”text” placeholder=”I am focused on load” />;} |
Output: When the component loads, the input field automatically receives focus. inputRef holds a reference to the actual DOM node, allowing direct interaction without triggering additional renders.
8. useLayoutEffect
useLayoutEffect is similar to useEffect. However, it runs synchronously after all DOM mutations have been committed. It is useful for measuring layout or making DOM updates that need to be applied before the browser paints. Use it sparingly, as it can block visual updates.
Syntax:
| useLayoutEffect(() => { // DOM measurement or mutation logic return () => { // cleanup (optional) };}, [dependencies]); |
Example:
| import React, { useState, useLayoutEffect, useRef } from ‘react’; function Tooltip() { const [tooltipHeight, setTooltipHeight] = useState(0); const tooltipRef = useRef(null); useLayoutEffect(() => { const height = tooltipRef.current.getBoundingClientRect().height; setTooltipHeight(height); console.log(‘Tooltip height measured before paint:’, height); }, []); return ( <div> <span>Hover me</span> <div ref={tooltipRef} style={{ position: ‘absolute’, top: ’20px’ }}> Tooltip content </div> <p>Tooltip height: {tooltipHeight}px</p> </div> );} |
Output: The tooltip height is measured and logged immediately after DOM updates but before the browser paints the screen. It prevents visual flicker that could occur if the same logic ran in useEffect.
To learn more about hooks in React.js and how they are implemented, you can pursue a React course.
Pro Tip: Understanding the concept and syntax is important. However, job interviewers often test your depth of understanding, not just syntax. To prepare effectively, explore our blog on React interview questions and answers to see how these concepts are commonly discussed in real interviews.
React Hooks Example
Questions about best practices and how to resolve frequent problems will inevitably arise, despite React Hooks having a relatively straightforward API and a large user base across diverse applications. Let’s look at a few examples:
Picture ref: https://blog.logrocket.com/react -hooks-cheat-sheet-solutions-common-problems/
- Declare State Variable
Declaring a state variable is the simplest way to add state to a functional component. The useState Hook takes an initial value and returns an array with two elements: the current state value and a function to update it. The initial value can be any data type, such as a string, number, boolean, object, array, or even null.
Example:
| import React, { useState } from ‘react’; const DeclareStateVar = () => { const [count, setCount] = useState(100); return <div> State variable is {count}</div>} |
- Create a State From a Function
When the initial state requires expensive computation or needs to be derived from a function (like reading from localStorage or performing calculations), you can pass a function to useState instead of a direct value. This function runs only once during the initial render, preventing unnecessary re-computation on subsequent renders. An example is seen below:
Example:
| import React, { useState } from ‘react’; const StateFromFn = () => { const [token, setToken] = useState(() => { let token = window.localStorage.getItem(“my-token”); return token || “default#-token#”; }); return <div>Token is {token}</div>;}; |
Pro Tip: Using a function to initialize state is a powerful pattern that prevents unnecessary re-execution of expensive logic. It is especially useful when you need to read from localStorage or perform one-time calculations. To better understand how state initialization fits into the broader component lifecycle, consider exploring React lifecycle methods.
3. Update State Variable
Updating a state variable is done by calling the updater function returned by useState. You can pass the new value directly or a function that takes the previous state and returns the new state. The function form is recommended when the new state depends on the previous state, as it ensures you’re working with the most up-to-date value.
Example:
| const UpdateStateVar = () => { const [age, setAge] = useState(19) const handleClick = () => setAge(age + 1) return ( <div> Today I am {age} Years of Age <div> <button onClick={handleClick}>Get older! </button> </div> </div> )} |
Pro Tip: Understanding how to update state properly is essential for building reliable React components. When state updates depend on previous values, use the functional update pattern to avoid unexpected bugs.
- Fetching Data With Loading Indicator
Fetching data asynchronously is a common side effect in React applications. Using useState and useEffect together, you can manage loading states, handle responses, and display data or errors appropriately. This pattern ensures users receive visual feedback while data is being fetched, providing a smooth user experience. The following are the commands you need to follow to fetch data with a loading indicator in React Hooks:
Example:
| import React, { useState, useEffect } from ‘react’; const FetchData = () => { const stringifyData = data => JSON.stringify(data, null, 2); const initialData = stringifyData({ data: null }); const loadingData = stringifyData({ data: ‘loading…’ }); const [data, setData] = useState(initialData); const [gender, setGender] = useState(‘female’); const [loading, setLoading] = useState(false); useEffect(() => { const fetchData = () => { setLoading(true); const uri = ‘https://randomuser.me/api/?gender=’ + gender; fetch(uri) .then(res => res.json()) .then(({ results }) => { setLoading(false); const { name, gender, dob } = results[0]; const dataVal = stringifyData({ …name, gender, age: dob.age }); setData(dataVal); }); }; fetchData(); }, [gender]); return ( <> <button onClick={() => setGender(‘male’)} style={{ outline: gender === ‘male’ ? ‘1px solid’ : 0 }} > Fetch Male User </button> <button onClick={() => setGender(‘female’)} style={{ outline: gender === ‘female’ ? ‘1px solid’ : 0 }} > Fetch Female User </button> <section> {loading ? <pre>{loadingData}</pre> : <pre>{data}</pre>} </section> </> );}; export default FetchData; |
Pro Tip: Practice each React Hook by building small, real projects. It helps you understand how hooks work in real scenarios and improves your confidence. If you need ideas to get started, check out our blog on React projects for beginners to find practical project ideas you can build step by step.
Best Practices for Using React Hooks
React Hooks are powerful, but when used incorrectly, they can lead to bugs, performance issues, and hard-to-maintain code. Following established best practices helps you write cleaner, more predictable components and avoid common pitfalls. The following are some recommendations for using React Hooks:
- Use Hooks Only in Functional Components: Hooks are designed specifically for functional components and do not work inside class components. Attempting to use them in classes will result in errors. Stick to functional components when using Hooks to keep your code consistent and error-free.
- Call Hooks at the Top Level: Always call Hooks at the top level of your component, not inside loops, conditions, or nested functions. It ensures React can correctly preserve state between renders. When Hooks are called in a different order each time, React loses track of which state belongs to which Hook, leading to hard-to-track-down bugs.
- Use useState for Simple State: For basic values like numbers, text, or true/false flags, useState is the right tool. It’s simple, readable, and does the job well. If you find yourself managing multiple related values or complex update logic, consider switching to useReducer instead.
- Handle Side Effects with useEffect: Use useEffect for tasks like fetching data, updating the DOM, or setting up event listeners. By default, effects run after every render. You can control when they run by passing a dependency array. Always clean up subscriptions or timers by returning a cleanup function to prevent memory leaks.
- Optimize with useCallback and useMemo: Use useCallback to keep functions from changing unnecessarily, and useMemo to avoid expensive recalculations. It can help your app run faster by preventing unnecessary re-renders. However, don’t overuse them, as adding them everywhere adds complexity. Use them only when you notice a performance issue or when passing functions to memoized child components.
- Share Data with React Context: When you need to pass data through multiple components without drilling down into props, useContext is a clean solution. It works well for global data like themes, user login status, or language preferences. For a more complex global state that changes frequently, consider using a state management library like Redux or Zustand.
- Keep It Simple: Use Hooks to make your code easier to read, not harder to read. If a Hook makes things more complicated, step back and consider a simpler approach. Sometimes, a few lines of plain JavaScript are better than a custom Hook. The best code is often the simplest code that still solves the problem.
Conclusion
Hooks in React.js have changed how developers build React applications. They offer a cleaner, more intuitive way to manage state, handle side effects, and share logic across components. In this blog, we explored what React Hooks are, walked through the most commonly used Hooks with practical examples, and discussed best practices to help you avoid common pitfalls. By following the rules provided, you can build reliable and scalable React applications.
FAQ’s
Answer: Hooks in ReactJS are special functions that let you use features like state and lifecycle methods in functional components. They allow you to manage state, handle side effects, and reuse logic without writing class components. Common examples include useState, useEffect, and useMemo.
Answer: useState is the most commonly used Hook. It allows functional components to manage local state, making it essential for almost every React application. Whether you’re handling form inputs, toggling UI elements, or storing simple data, useState is typically the first Hook developers reach for.
Answer: Use useEffect when you need to handle side effects such as API calls, subscriptions, or DOM updates after rendering. On the other hand, use useMemo to optimize performance by caching the result of expensive calculations and avoiding unnecessary recomputation.
