React Hooks
What are React Hooks?
React Hooks are special functions introduced in React 16.8 that allow you to "hook into" React state and lifecycle features from function components. Before Hooks, these features were exclusive to class components. With Hooks, it's possible to write React components entirely with functions, making the code more concise, readable, and easier to test and reuse. They solve common problems faced with class components, such as the complexity of this, the reuse of logic with HOCs (Higher-Order Components) and Render Props, and the difficulty of organizing related code in lifecycle methods.
What are React Hooks for?
React Hooks serve a variety of purposes, primarily to simplify the development of React components and make them more functional and reusable. The main objectives and benefits of Hooks include:
Reusing Logic with State: Before Hooks, reusing stateful logic between components was complex, often requiring patterns like Higher-Order Components (HOCs) or Render Props. Hooks like useState and useEffect allow extracting stateful logic from a component into a reusable function, which can be applied to multiple components without the need to restructure the component tree.
Simplifying State Management: useState offers a more direct and functional way to add state to function components, eliminating the need for class components and the complexity associated with this and setState.
Managing Side Effects: useEffect centralizes side effect logic (such as data fetching, direct DOM manipulation, subscriptions) in a single place, making it easier to understand and manage. It replaces the componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle methods in a more declarative way.
Simplified Context: useContext simplifies consuming context in function components, eliminating the need for render props or HOCs to access context data.
Reducing Class Component Complexity: Hooks allow developers to write most of their components as functions, which generally results in cleaner, less verbose, and easier-to-reason-about code. This eliminates the need to understand the nuances of this in JavaScript and how lifecycle methods work in class components.
Better Code Organization: Hooks allow grouping related logic in a single place. For example, subscription logic and its cleanup can be kept together in a useEffect, instead of being spread across componentDidMount and componentWillUnmount.
Improved Performance (Potential): While not a direct guarantee of performance, the functional nature of Hooks and the way they encourage writing more optimized code can lead to performance improvements in certain situations, especially when combined with useCallback and useMemo for rendering optimizations.
In summary, React Hooks are a powerful tool that modernizes how we write React components, making development more efficient, code cleaner, and logic easier to reuse and maintain.
When to use each Hook?
React provides a variety of built-in Hooks to handle different aspects of component development. Here are the most common Hooks and when you should use them:
useState
Allows you to add state to function components. It returns a pair: the current state value and a function to update it. When to use:
When your function component needs to maintain and manage data that can change over time and affects the component's rendering.
To manage simple states like counters, toggles (true/false), form inputs, etc.
Example usage:
useEffect
Allows you to perform side effects in function components. Side effects are operations that affect the outside world of the component, such as data fetching, direct DOM manipulation, subscriptions, timers, etc. When to use:
To fetch data from an API after the component mounts.
To set up or clean up subscriptions (e.g., mouse events, timers).
To directly manipulate the DOM (e.g., change the page title).
For any logic that needs to run after the component renders and may have dependencies.
Example usage:
useContext
Allows you to subscribe to a React context without nesting components. The Context API is a way to pass data through the component tree without having to manually pass props at each level. When to use:
When you need to share data that is considered "global" to a component tree (e.g., theme, authenticated user information, language settings).
To avoid "prop drilling" (passing props through many levels of components).
Example usage:
useRef
Returns a mutable ref object whose .current property is initialized with the passed argument (initialValue). The returned object will persist for the entire lifetime of the component. When to use:
To directly access a DOM element (e.g., focusing an input, playing/pausing a video).
To store a mutable value that doesn't cause a re-render when changed (e.g., a timer ID, a reference to a non-React object).
Example usage:
useMemo
Returns a memoized value. It only recalculates the memoized value when one of the dependencies changes. This can be useful to avoid expensive calculations on every render. When to use:
To optimize performance by avoiding unnecessary recalculations of expensive computed values.
When you have a pure function that returns a value and that function is called frequently with the same inputs.
Example usage:
useCallback
Returns a memoized function. It only returns a new function when one of the dependencies changes. This is useful for optimizing child components that depend on functions passed as props, preventing unnecessary re-renders. When to use:
To optimize the performance of child components that are unnecessarily re-rendered because a callback function is recreated on every render of the parent component.
When you pass callbacks to child components optimized with React.memo.
Example usage:
useReducer
An alternative to useState for managing more complex states that involve multiple sub-properties or when the next state depends on the previous state. It is often used with a reducer (a function that receives the current state and an action, and returns the next state). When to use:
To manage complex states that involve more elaborate state transition logic.
When a component's state is updated based on more complex actions (similar to Redux).
When the next state depends on the previous state.
Example usage:
useImperativeHandle
Customizes the instance value that is exposed to parent components when using ref. When to use:
In conjunction with forwardRef, when you need to expose specific methods or properties of a child component to a parent component, instead of exposing the full DOM instance.
To create imperative APIs for components.
Example usage:
useLayoutEffect
It's identical to useEffect, but it fires synchronously after all DOM mutations. Use it to read DOM layout and synchronously re-render. When to use:
When you need to perform DOM measurements (e.g., get an element's width) and then make a state update that affects the layout, to avoid flickering on the screen.
For effects that need to run before the browser paints the screen.
Example usage:
useDeferredValue
Allows you to defer the update of a value, which can be useful for improving UI responsiveness in scenarios where a part of the UI is expensive to render and doesn't need to be updated immediately. When to use:
When you have user input (like a search field) that triggers an expensive render (like a filtered list), and you want the UI to remain responsive while the expensive render is happening.
Example usage:
useTransition
Allows you to mark state updates as transitions, meaning they can be interrupted and don't block the UI. This is useful for keeping the UI responsive during state updates that may take some time. When to use:
When you have state updates that can be time-consuming and you want the UI to remain interactive during these updates.
To prioritize more urgent UI updates (like text input) over less urgent updates (like rendering a large list)
Example:
The list is long. These are just a few examples of React Hooks usage.
Good Practices when Using React Hooks
To make the most of React Hooks and write clean, efficient, and easily maintainable code, it's important to follow some good practices:
Follow the Rules of Hooks:
Call Hooks only at the top level: Do not call Hooks inside loops, conditions, or nested functions. This ensures that Hooks are called in the same order on every render, which is crucial for React to correctly associate state and effects.
Call Hooks only from React functions: Call Hooks from React function components or from custom Hooks. Do not call them from regular JavaScript functions.
Use useState for Simple State and useReducer for Complex State:
For simple states (numbers, strings, booleans, small arrays or objects), useState is sufficient and more concise.
For more complex states that involve multiple sub-properties, or when state updates depend on the previous state in a complex way, useReducer is generally a better choice. It centralizes state update logic and can be easier to test.
Manage Side Effects with useEffect:
Use useEffect for all operations that interact with the outside world of your component (API requests, DOM manipulation, subscriptions, timers).
Effect Cleanup: Always return a cleanup function from useEffect when your effect needs cleanup (e.g., removing event listeners, canceling timers, closing subscriptions). This prevents memory leaks and unexpected behavior.
Dependency Array: Always specify the dependency array for useEffect. If the array is empty ([]), the effect will run only once (conceptually on componentDidMount and componentWillUnmount). If you omit the array, the effect will run on every render. If you include dependencies, the effect will re-run only when those dependencies change. Be careful not to forget dependencies, as this can lead to subtle bugs.
Optimization with useMemo and useCallback:
Use useMemo to memoize expensive values that don't need to be recalculated on every render unless their dependencies change. This is useful for avoiding unnecessary recalculations of data or components.
Use useCallback to memoize callback functions that are passed as props to child components optimized with React.memo. This prevents child components from unnecessarily re-rendering when the callback function is recreated on every render of the parent component.
Don't optimize prematurely: Use useMemo and useCallback only when there is a real performance problem. They add a small overhead and can make the code less readable if used excessively.
Create Custom Hooks for Reusable Logic:
If you have stateful logic or side effects that are used in multiple components, extract it into a custom Hook. Custom Hooks are JavaScript functions that start with use and can call other Hooks.
This promotes code reuse, improves readability, and facilitates testing.
Use useRef for Imperative References and Mutable Values:
Use useRef to directly access DOM elements or to store mutable values that don't need to trigger a re-render (e.g., a timer ID, a class instance).
Avoid using useRef to manage component state that affects rendering; for that, use useState or useReducer.
Avoid Direct State Mutations:
When updating state with useState or useReducer, always return a new object or array instead of mutating the existing state. This ensures that React detects changes and renders the component correctly. Example: setArray([...array, newItem]) instead of array.push(newItem).
Context API with useContext for Global Sharing:
Use the Context API with useContext to share data that is considered "global" to a part of the component tree, avoiding "prop drilling."
Do not use Context to pass props that change very frequently, as this can lead to unnecessary re-renders in components consuming the context.
Test Your Hooks:
Test your custom Hooks in isolation to ensure they work as expected and that the state and side effect logic is robust.
By following these good practices, you can write more efficient, readable, and maintainable React components using Hooks.
React Hooks have revolutionized the way we write components in React, offering a powerful and more functional alternative to class components. They allow developers to use state and lifecycle features directly in function components, resulting in cleaner, more concise, and modular code. By understanding the purpose of each Hook and applying the recommended best practices, it is possible to build more efficient, scalable, and maintainable React applications. The adoption of Hooks not only simplifies state and side effect management but also promotes logic reuse and performance optimization, making development with React a more enjoyable and productive experience.
Senior Sotfware Engineer | React | Node | Python | Django | Docker | RabbitMQ | AWS
1wGreat post — clear, actionable, and full of value! One mindset shift that really helped me level up with Hooks was thinking in terms of “behavior composition” instead of component inheritance or lifecycle juggling. With custom hooks, we essentially create mini behaviors — decoupled, testable, and shareable — which naturally leads to more maintainable codebases.
SDET | QA Engineer | Test Automation Engineer | Playwright | Cypress | Robot Framework | Postman | Cucumber | Jenkins | Typescript | Javascript | Python | Manual Testing | Jira
1wThoughtful post, thanks Fernando.
Back End Engineer | Software Engineer | TypeScript | NodeJS | ReactJS | AWS | MERN | GraphQL | Jenkins | Docker
1wThanks for sharing 🚀
--Frontend Developer | HTML, CSS, JavaScript Learner | #WebDevelopment
2wThanks for sharing! React really changed the way I build web interfaces. I just learned about Hooks and find useEffect extremely powerful. Do you have any advice for more effective state management in large projects?
Software Engineer | Java | Kotlin | Spring Boot | AWS | Docker | Kubernetes | Microservices | Backend
2wAwesome summary! Hooks were a total game-changer for React — they not only simplified component logic but also made code far more reusable and expressive. Really appreciate that you covered both the “why” and the practical use cases. Curious to know: do you have a preferred pattern for organizing custom hooks in larger codebases?