Exploring ReactJS Hooks: A Comprehensive Tutorial

Posted by Alice Brainna on January 24th, 2024

React Hooks were introduced in React 16.8 as a new way to use state and other React features in functional components. Prior to hooks, stateful logic in React was only possible using class components.

Hooks allow function components to have access to state and other React features like lifecycle methods. This removes the need for class components in many cases.

Some key benefits of using hooks compared to classes:

  • Hooks enable splitting complex components into smaller functions based on related features/logic. This makes code easier to understand, test, and reuse.

  • Hooks avoid the this keyword entirely. No more binding issues or confusion around this.

  • Hooks embrace functions. Conceptually, React components were already like functions. Hooks fully embrace functions and avoid classes.

  • Hooks have much cleaner code and avoid giant wrapper classes when you need multiple stateful values. Each stateful value can have its own hook.

  • Hooks avoid breaking the Rules of Hooks. Classes have methods like componentDidMount that must be remembered to call super.

Overall, hooks represent a simpler mental model for stateful logic in React. They provide a more direct API into React concepts like state and lifecycle events. Many find hooks easier to understand than classes in React.

Using State Hooks

The useState Hook allows you to add state to your functional components. It returns an array with two values - the current state value and a function to update it.

For example, if we want to create a counter:

import { 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>

  );

}

We initialize the count state to 0 by passing 0 to useState. The setCount function allows us to update the state.

useState is also useful for toggles and other simple state:

// Toggle

const [isOpen, setIsOpen] = useState(false);

// Form values

const [name, setName] = useState('Mary');

const [email, setEmail] = useState('');

The useState Hook replaces the need to use a class component just to track state. We can now manage state from our function components with Hooks!

Using Effect Hooks

The Effect Hook, useEffect, adds the ability to perform side effects from a function component. Some common side effects are: fetching data, directly updating the DOM, and timers.

useEffect accepts two arguments. The first argument is a function that contains the side effect logic. The second argument is an optional array of dependencies.

Here are some common use cases for useEffect:

Making API Calls

You can call an API inside of useEffect and perform additional side effects based on the result:

import { useState, useEffect } from 'react';

function MyComponent() {

  const [data, setData] = useState(null);

  useEffect(() => {

    async function fetchData() {

      const response = await fetch('http://example.com/api');

      const json = await response.json();

      setData(json);

    }

    fetchData();

  }, []); // Only call API once, on mount

  // ...

}

DOM Manipulation

You may need to imperatively manipulate the DOM by adding/removing event listeners, measuring an element, etc. useEffect is well-suited for this:

useEffect(() => {

  const button = document.getElementById('my-button');

  button.addEventListener('click', () => {

    // Handle click

  });

  return () => {

    button.removeEventListener('click');

  }

}, []);

The return function is used for cleanup before the component unmounts.

Timers

setTimeout and setInterval can be cleaned up automatically with useEffect:

useEffect(() => {

  const id = setInterval(() => {

    // ...

  }, 1000);

  return () => clearInterval(id);

}, []);

As you can see, useEffect enables us to consolidate many types of side effects into a single API.

Rules of Hooks

React Hooks have two simple rules:

  • Only call Hooks at the top level of your React functions. Don't call Hooks inside loops, conditions, or nested functions.

  • Only call Hooks from React function components. Don't call Hooks from regular JavaScript functions.

These rules exist to make sure Hooks behave predictably and consistently.

The React team introduced Rules of Hooks to avoid confusing behavior from nested function calls. Without the rules, Hooks would be hard to understand when reading the code later. The rules make the control flow easy to follow.

Hooks were designed to only be used in React function components, not in regular JavaScript functions. This convention keeps component stateful logic and effects within the React component. It prevents you from accidentally calling a Hook in the wrong place.

Following these simple rules prevents bugs. Your components will behave as expected when you call Hooks at the top level of React function components unconditionally.

Custom Hooks

Custom Hooks allow you to extract component logic into reusable functions. This can help reduce duplication and improve code organization. There are a few main motivations for creating custom hooks:

  • Share logic between components - Extract commonly used logic into a custom hook to avoid rewriting the same code.

  • Simplify complex components - Custom hooks allow you to break complex components into smaller functions based on logical concerns.

  • Separation of concerns - Enforces separation between UI and business logic. The component uses the hook, while the hook encapsulates the stateful logic.

Some common scenarios where custom hooks are useful:

  • Data fetching - Encapsulate data fetching and caching logic that can be reused across components.

  • UI/Data syncing - Synchronize UI state with external data sources like web APIs.

  • Lifecycle events - Centralize handling for events like componentDidMount and componentWillUnmount.

  • State persistence - Persist state across render cycles, sessions, tabs etc.

  • Timers - Manage setTimeout, setInterval timers.

  • Event listeners - Attach and clean up event listeners.

  • Animations - Manage animation state and play/stop callbacks.

  • Third party libraries - Wrap third party library hooks to streamline usage in your app.

The key is spotting repetitive logic across components that can be extracted into reusable custom hooks. This helps you build a toolkit of hooks that can be shared across your application.

Other Built-in Hooks

React provides a few other built-in hooks that are handy in some cases:

  • useContext - Allows you to avoid passing props through intermediate elements and directly access/update context from any component. Helpful for accessing things like themes, UI settings, etc globally.

  • useReducer - An alternative to useState if you need more complex state management with a reducer function instead of setState. Works well with managing state in large apps.

  • useCallback - Returns memoized callback function to optimize performance by avoiding unnecessary re-renders when passing callbacks.

  • useMemo - Returns memoized value to optimize performance by caching results of expensive functions to avoid repeating calculations unnecessarily.

  • useRef - Gives access to a mutable ref object to persist values between renders. Commonly used to reference DOM elements.

  • useImperativeHandle - Customize the exposed instance value for refs created with React.createRef(). Similar use cases as forwardRef.

  • useLayoutEffect - Identical to useEffect but fires synchronously after DOM mutations. Use for DOM reads/writes to avoid visual bugs.

  • useDebugValue - Display a label in React DevTools for custom hooks. Helpful for debugging.

While you won't use these in every React component, they provide powerful capabilities in certain use cases. The React docs cover them in detail with examples of when to use each one.

Hook Dependency Array

The dependency array in React hooks allows you to optimize performance and avoid unnecessary re-renders. It's the second argument passed to the useEffect, useMemo, and useCallback hooks.

Purpose of the Dependency Array

  • The dependency array allows you to specify which values from props or state React should "watch" to determine if a re-render is needed.

  • If the watched values haven't changed since the last render, React will skip the re-render to optimize performance.

  • An empty dependency array [] will tell React to only re-render the component on mount and unmount.

Common Mistakes

Some common mistakes when using the dependency array include:

  • Forgetting to add dependencies - this can lead to stale values and bugs

  • Overusing the empty dependency array [] - re-renders won't happen even if props/state change

  • Adding unnecessary dependencies like props/state that aren't used in the effect - can impact performance

How to Avoid Mistakes

  • Only add variables from props/state that are used inside the hook callback

  • Use a linter rule to warn when dependencies are missing

  • Compare dependencies before and after a state change to see if a re-render should happen

  • Wrap functions inside useCallback hooks instead of directly adding as dependencies

  • Avoid overly complex logic inside hooks that depend on many variables

The dependency array takes some practice to use correctly. Adding logging or React debugging tools can help trace issues. Overall, include the minimal dependencies needed to allow necessary re-renders for component updates.

Performance Optimizations

When working with React Hooks, there are several techniques you can use to optimize performance:

  • Memoization - This technique caches the result of function calls to avoid unnecessary re-renders. React provides the useMemo and useCallback hooks for this purpose.

  • useMemo - This hook memoizes the result of a function passed to it. It only recomputes the memoized value when one of the dependencies has changed. This helps avoid expensive calculations on every render:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); 

  • useCallback - This hook memoizes a callback function. It's useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders:

const memoizedCallback = useCallback(() => {

  doSomething(a, b);

}, [a, b])

  • Virtualization - This renders only visible DOM elements to reduce memory usage and speed up rendering. React provides React.memo for component memoization and React Virtual for list virtualization.

  • Dynamic Import - This technique code-splits your app into separate bundles that are loaded on-demand. Suspense and React Loadable help with loading states.

  • Windowing - This only renders a subset of rows in a large list for better optimization. react-window provides windowing for lists, tables, and grids.

In summary, utilizing techniques like memoization, virtualization, dynamic import, and windowing can greatly optimize the performance of React Hooks. The key is to identify bottlenecks and apply optimizations prudently.

Migration Strategies

Incrementally migrating existing code from React classes to hooks can make adopting hooks easier. Here are some strategies for incremental migration:

  • Start by converting one component at a time. Choose a non-critical component and convert it to use hooks. This will allow you to get familiar with hooks without disrupting your whole app.

  • Convert sibling components together. Components that share logic or state are good candidates to migrate together. This allows you to extract shared stateful logic into custom hooks.

  • Create new features with hooks first. Use hooks when building any new features or components. This avoids adding more classes that would eventually need migrated.

  • Convert by file or folder. Migrate all components in a single file or folder at once. This makes managing the migration simpler by avoiding a mix of classes and functions within files.

  • Gradually refactor legacy class logic. As you gain confidence with hooks, start cleaning up and simplifying legacy class components by extracting logic into custom hooks.

  • Use codemods to automate refactors. Tools like react-codemod can help automate migration tasks like converting class components to functions.

The key is to take the migration step-by-step. Going slowly reduces risk and allows your team to build familiarity with hooks before fully migrating your codebase.

Conclusion

React Hooks provide a powerful way to manage state and side effects in functional components. Here's a summary of the main points we covered:

  • UseState and UseEffect hooks are alternatives to state and lifecycle methods in class components

  • Only call hooks at the top level of a component, not in loops or conditions

  • Custom hooks let you extract reusable logic into standalone functions

  • Other handy hooks like UseContext and UseReducer exist for different use cases

  • Pay attention to dependencies passed to hooks like useEffect to avoid bugs

  • Using hooks properly can optimize performance and prevent unnecessary re-renders

  • There are various strategies to migrate class components to hooks gradually

There's a lot more to discover about React Hooks! The React docs are a great place to learn more details and see additional examples. The React community also creates handy custom hooks you can install from npm and use in your own components.

Visit for More

And there you have it – a whirlwind tour of ReactJS hooks! We've only scratched the surface, so if you're hungry for more, the React documentation is your best friend.

Before you go, if you're gearing up for a React-powered project, remember to Hire ReactJS Developers who can make the most out of these hooks and create robust, scalable applications.

Like it? Share it!


Alice Brainna

About the Author

Alice Brainna
Joined: October 29th, 2020
Articles Posted: 15

More by this author