Understanding React Hooks

July 21, 2023

What are React Hooks?

React Hooks are functions provided by React that lets a developer hook into React features from the function component. React Hooks brought lots of advantages to the development ecosystem which fixes the issue faced with class components.

Hooks are new React APIs added to React 16.8. They enabled React functional components to use React features that were previously only available in React class components. They are functions that deliver the power of React class components to functional components, resulting in a cleaner way to combine them.

Functions that starts with the use word are called Hooks. Hooks are more restrictive than other functions. Hooks are only called at the top of your components. Before the existence of Hooks prior to February 16, 2019, React functional and class components performed distinct functions. Functional components were only used for presentation purposes.

Functional Components

A functional component is simply a plain JavaScript function which accepts props as an argument and returns a React element. React assumes that every component you write is a pure function which means that React components written must always returns the same JSX given the same inputs. Functional components does not allow setState() and why they are referred to as stateless components. When state is required, a class component is created or the state is lifted up to the parent component and have the state pass down via props to the functional component. A functional component cannot use lifecycle hooks because they are coming from React.component which is extended from class components. Functional components does not keep track of an internal state and doesn’t know the component lifecycle. Thus, they were referred to as “dumb components.

Example of Functional Components with Props

function Recipe({ props }) {
  return (
    <div>
      <p>The prop passed in is: {props}</p>
    </div>
  );
}

export default function App() {
  return (
    <section>
      <Recipe props="I am a prop from parent component" />
    </section>
  );
}

Class Components

Class components track a component’s internal state and enable you to perform operations during each phase by using lifecycle methods. One way to see class component in action is making a fetch request to an external API once a component mounts, update, the state is updated with a user’s interactivity, and unsubscribe from a store once a component unmounts. This is made possible due to the class component keeping track of its internal state and lifecycle. React Hooks were added to solve wrapper hell, huge components, and confusing classes among others that developers faced using class components. However, some of these issues are not connected to React directly, but rather the way native JavaScript classes are designed.

Example of Class Components with Props

import { Component } from 'react';

class Greeting extends Component {
  render() {
    return <h1>Hi, {this.props.name}!</h1>;
  }
}

export default function App() {
  return (
    <>
      <Greeting name="Adams" />
      <Greeting name="Ben" />
      <Greeting name="Cathrine" />
    </>
  );
}

Challenges in React Class Components

React class components is a native JavaScript class. It inherited the issues of JavaScript classes including working with ‘this’ keyword, explicitly binding methods, verbose syntax among others.

Binding Methods and Working with this Keyword

import React, { Component } from 'react';

class ExampleComponent extends Component {
   constructor(props) {
      super(props);
      this.state = { name: 'John Doe' };
      this.changeName = this.changeName.bind(this);
   }

   changeName() { 
      this.setState({ name: 'Abel Doe' });
   }

   render() {
      return (
         <div>
            <p>My name is {this.state.name}.</p>
            <button onClick={this.changeName}>Change name</button>
         </div>
      );
   }
}

The above code displays a simple class component that renders a name state to the UI and provides a button to change the name. As seen, you have to bind ‘this’, call super(props) in the base constructor, and always prefix your state or methods with ‘this’ to access them.

The expressions above are a function of how ES6 classes are designed and are some of the common causes of bugs in React applications.

Verbose Syntax

React class components have a verbose syntax that can often result in very large components; components with lots of logic split across lifecycle methods which can be hard to read and follow. The lifecycle method API forces you to repeat logic across different lifecycle methods throughout the component. Example below.

import React from "react";

class Profile extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false,
      profile: {}
    }
  }

  componentDidMount() {
      this.subscribeToprofilesStatus(this.props.id);
      this.updateProfile(this.props.id);
  }

  componentDidUpdate(prevProps) {
      // compariation hell.
      if(prevProps.id !== this.props.id) {
          this.updateFriendProfile(this.props.id);
      }
  }

  componentWillUnmount() {
      this.unSubscribeToFriendsStatus(this.props.id);
  }

  subscribeToprofilesStatus() {
    console.log("I have subscribled")
  }

  unSubscribeToFriendsStatus () {
    console.log("I have unsubscribled")
  }

  fetchFriendData(id) {
    // fetch friend logic here
  }

  async updateProfile(id) {
    this.setState({loading: true})

    // fetch friend data
    await this.fetchFriendData(id);

    this.setState({loading: false})
  }

  render() {
    return (<div> Hello {this.friend ? this.friend.name : "John Doe!"}</div>); // ... jsx
  }

}

export default FriendProfile;

Difficult to Reuse and Share Logic

To compose React class components, you often need complicated patterns such as the Higher-Order Components (HOC) pattern that make your code difficult to read and maintain.

HOCs are often used for cross-cutting concerns like authorization, logging, and data retrieval; that is, tasks that span the entire application and would otherwise lead to repeated logic. The HOC pattern leverages higher-order functions in JavaScript—an HOC is a pure function with zero side effects.

Consider the code below:

import React, { Component } from "react";

const Logger = (WrappedComponent) => {
  return class LoggerHOC extends Component {
    state = {
      name: "John Doe",
      logInfo: true
    };

    componentDidMound() {
      // some logic
    }

    componentDidUdpate() {
      // some logic
    }

    render() {
      return <WrappedComponent {...this.state} {...this.props} />;
    }
  };
};

export default Logger;

HOC Usage

import React from "react";
import Logger from "./Logger";

const Info= (props) => {
  return <div>Hello, my name is {props.logInfo && props.name}</div>;
};

export default Logger(Info);

This is a simple clear HOC but the issue arises when multiple HOCs are required in application. Take for instance, an application requires authentication, theme, logger and router, and you have the following.

import React from "react";
import WithRouter from "./components/WithRouter";
import WithAuth from "./components/WithAuth";
import WithLogger from "./components/WithLogger";
import WithTheme from "./components/WithTheme";

const SomeComponent = (props) => {
  return (
    // some jsx
    )
}

export default WithTheme(
  WithAuth(
    WithLogger(
      WithRouter(SomeComponent);
    )
  )
);

The above expression can lead to codes/scripts that are difficult to read or interpret.

Also, creating HOCs can result in deeply nested structure in the React dev tool and code debugging becomes a nightmare.

The Introduction of React Hooks and Help

Hooks solves all of the class-related problems listed above. They also enable you to write cleaner, leaner, and more maintainable code.

This section talks about the useState, useEffect, useRef, useContext, and custom Hooks in more detail.

The useState Hook

The useState Hook gives you an easy way to use state in a functional component. UseState is a React Hook that lets you add a state variable to your component.It also takes one argument (the initial state) and returns an array with two values: the current state and a function to update the state. By convention, these values are stored using array destructuring.

Here is the function signature of the useState Hook: jsx

const [state, setState] = useState(initialState); You can rewrite the Card class component above to a functional component by using the useState Hook, as seen here:

import React, { useState } from "react";

const Card = () => {
  const [name, setName] = useState("John Doe");

  return (
    <div>
      <p>Hello, from SayName. My name is {name}</p>
      <button onClick={() => setName("Jane Doe")}>Change Name</button>
    </div>
  );
};

export default Card;

Above, Hooks enables you to use states without dealing with class constructors and binding ‘this’. This is a much easier and simpler approach compared to class components.

The useEffect Hook

useEffect is a React Hook that lets you synchronize a component with an external system.

It gives you an easier way to hook into a component lifecycle without writing redundant logic like we have in class components.

useEffect(() => {
   // mounting

   return () => {
      // unmounting
   }
}, [
  // updating
])

The function signature of the useEffect Hook is in the code. It takes two arguments: a function that is called after each complete render and a dependency array.

The function passed to the useEffect Hook contains the logic that executes side effects. If you want to do a clean up, as you do with componentWillUnmount in a class component, return a function from this that is passed to the useEffect Hook.

Lastly, the array in the second argument holds a list of dependencies used for updating the component.

You can refactor your Profile component to the code below:

import React, { useState, useEffect } from "react";

function Profile({ id }) {
  const [loading, setLoading] = useState(false);
  const [profile, setProfile] = useState({});

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    updateProfile(id);
    subscribeToStatus(id);

    return () => {
      unSubscribeToStatus(id);
    };
  }, [id]);

  const subscribeToStatus = () => {
    console.log("I have subscribled");
  };

  const unSubscribeToStatus = () => {
    console.log("I have unsubscribled");
  };

  const fetchData = (id) => {
    // fetch friend logic here
  };

  const updateProfile = async (id) => {
    setLoading(true);

    // fetch friend data
    await fetchData(id);

    setLoading(false);
  };

  return <div> Hello {friend ? friend.name : "John Doe!"}</div>; // ... jsx
}

export default Profile;

Custom Hooks

You can share non-visual logic by creating custom Hooks. This will allow you to reuse logic across your components, consequently keeping your codes DRY.

The isMounted Hook is a good example of a custom Hook. It ensures you do not “set state” when a component is unmounted. In React, mutating the state on an unmounted component will log a warning error in the console:

// Warning: Can't perform a React state update on am unmounted component. 
// This is a no-op, but it indicates a memory leak in your application.
// To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

This is because when a component is unmounted in React, it will never be mounted again. You can handle this by conditionally mutating a component’s state based on this.isMounted, as seen below:

if(this.isMounted()) { // Bad code
   this.setState({...};
}

Although this may work, the React team considers it an antipattern and instead, recommends that you track the value mounted status yourself.

To do this effectively, create a custom Hook that tracks the status of mounted. You can use this Hook across your components.

import { useEffect, useState } from "react";

const useIsMounted = () => {
  const [isMounted, setIsMouted] = useState(false);
  useEffect(() => {
    setIsMouted(true);
    return () => setIsMouted(false);
  }, []);
  return isMounted;
};
export default useIsMounted;

Usage:

//...
const Dev = () => {
  const isMounted = useIsMounted();

  const [state, setState] = useState("");
  useEffect(() => {
    function someFunc() {
      setTimeout(() => {
        if (isMounted) setState("Lawrence Eagles");
      }, 4000);
    }
    someFunc();
  });

  return (
    // ... jsx blob
  );
}

The useRef Hook

The useRef Hook takes an initial value and returns a ref, or reference object. This is an object with a current property that is set to the initial value of the useRef.

Signature:

const refContainer = useRef(initialValue);

The useRef Hook is used for creating refs that gives you direct access to DOM elements. This is useful when working with forms.

const formComponent = () => {
  const inputElem = useRef(null);

  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputElem} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

Also, ref is a mutable JavaScript object that can persist on every rerender, making useRef useful for keeping the mutable value around.

The useContext Hook

The React context API gives you a way to share data across your components without prop drilling. With the useContext Hook, you can easily use the context API from a functional component.

Signature:

const value = useContext(MyContext);

The useContext Hook takes a context object (the value returned from React.createContext) as its parameter and returns the current context value (the value of the nearest context provider component). When this provider component updates, the Hook triggers a rerender.

import React, {createContext, useContext} from "react";

const themes = {
  light: {
    name: "Light",
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    name: "Dark",
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Button />
    </ThemeContext.Provider>
  );
}

const Button = () => {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      {theme.name} Button
    </button>
  );
}

Conclusion

As you’ve just seen, React Hooks can help solve many problems associated with React class components. Hooks are simple, composable, flexible, and extendable, which are major pros. Despite this, there are a lot of challenges, including how Hooks handle stale state, access state in an asynchronous callback, and access state synchronously.

By passing an updater function to the setter function (setState), stale state can be prevented. Also, accessing state in an asynchronous callback may be impossible but by persisting the mutable state value on every rendering using useRef Hook is one way to resolve the issue.

Since useState works asynchronously, accessing state synchronously seems impossible. By assigning the next update value to a variable and use that variable to update the state is a better approach.

As we discussed earlier, Hooks are a collection of specialized JavaScript functions that aim to solve the issues you may experience when working with React class components. They enable functional components to use React features only available with React classes by giving you direct and flexible access to these features. Clearly, Hooks have changed the way React components are created for the better—and are here to stay!