Full Stack Developer

Make Functional Components Behave Like Stateful Using React Hooks

React Hooks have become part of our production level applications after being released in version 16.8 in 2019. These hooks allow React developers to make functional components behave like stateful components and avoid Class Components.

useEffect is one of the most widely used Hooks allowing the developers to create conditional changes that reference the program state within a functional component. Let’s dig down more about how to use all three important built-in React Hooks.


What are React Hooks?

React has Functional Components that do not hold an internal state and Class Components that add stateful logic to the program and allow you to use lifecycle methods.

Many developers opposed this approach, as Class Components require ES6 classes to maintain internal states.

React Hooks offer an alternative.

React Hooks are functions that allow you to hook into React state and lifecycle features from function components. This allows you to use React without classes, which are widely disliked due to their reliance on JavaScript this calls. The best part is, Hooks are opt-in and work with existing code.

There are several built-in Hooks, like useEffect or useState, that reference common internal states. You can also create custom Hooks that reference states of your choice.

The most popular built-in Hooks are:

  • useState – Returns a stateful value and a function to edit it. Think of this as the Hook equivalent of this.state and this.setState in Class Components.
  • useEffect – Perform side effects from function components. These are queued for after a re-render to allow for limited iterative behavior in React.
  • useContext – Accepts a context object and returns current context value. Triggers a re-render next time the nearest MyContext.Provider updates.

Here are some advantages of React Hooks:

  • Better code composition: Hooks allow lifecycle methods to be written in a linear, render flowing order rather than splitting them among relevant Class Components.
  • Reuse states and components: Hooks make it easier to share stateful logic between different components. You use the same Hook to call states throughout a program rather than just within the same Class.
  • Better Testing: Hooks consolidate stateful logic so it’s all defined in a relevant Hook and is, therefore, easier to test.
  • Performance: When optimized, React Hooks are the fastest form of functional components.

Comparing Class implementation and React Hook implementation

Hooks are designed to be capable of everything Classes can do and more. Let’s see how we can update some old React code to use Hooks instead.

Here’s our old React code without Hooks:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      message: ''
    };
  }  componentDidMount() {
    this.loadMessage();
  }  loadMessage = async () => {
    try {
      const response = await axios.get('https://json.versant.digital/.netlify/functions/fake-api/message');
      this.setState({ message: response.data });
    } catch (e) {
      this.setState({ message: e.message });
    }
  };  render() {
    return <h1>{this.state.message}</h1>
  }
}

This code uses the componentDidMount method and this.setState to reference and manipulate the message status. These features can be replaced by the useEffect and useState Hooks.

To convert the code, we’ll:

  • Use the useState Hook to manage the message state
  • Replace componentDidMount method with the useEffect Hook
  • Set a message state using the function provided by useState hook

Here’s what the same React app looks like using Hooks:

import React, {
    useEffect, useState
} from 'react';
import axios from 'axios';

const INITIAL_MESSAGE = '';
const App = () => {
    const [message, setMessage] = useState(INITIAL_MESSAGE);
    useEffect(() => { loadMessage() }, []);
    const loadMessage = async () => {
        try {
            const response = await axios.get('https://json.versant.digital/.netlify/functions/fake-api/message');
            setMessage(response.data);
        } catch (e) {
            setMessage(e.message);
        }
    }; return <h1>{message}</h1>;
};
export default App;

As you can see, it’s easy to convert apps to use Hooks, and doing so will result in more readable code!

What is the useEffect Hook?

useEffect is one of the most popular Hooks because it allows you to perform side effects in function components. Let’s take a deeper look at the useEffect Hook to understand how that works.

The useEffect Hook lets you run additional code after React has already updated the DOM.

Think of the useEffect Hook as a partial replacement for React lifecycle events. The useEffect Hook can replicate the behavior of componentDidMountcomponentDidUpdate and componentWillUnmount methods.

In other words, you can respond to changes in any component that contains the useEffect Hook.

React Hooks Syntax

The useEffect Hook takes two arguments:

useEffect(() => {    // some code  }, [someProp, someState]);

The first argument is a callback function that by default runs after every render.

The second argument is an optional Dependency array that tells the Hook to only callback if there is a change in a target state. The Hook compares the previous and current state value of each dependency. If the two values don’t match, the Hook uses the first argument callback.

Dependency arrays override the default callback behavior and ensure the Hook ignores everything else in the component scope.

Use cases

Some common use cases of useEffect are:

  • Add an event listener for a button
  • Data fetching from API when component mounts
  • Perform an action when state or props change
  • Clean up event listeners when the component unmounts

In each case above, useEffect is used in place of a lifecycle method.

Using Dependencies Array with useEffect Hook

It’s important to use Dependency Arrays correctly to optimize your useEffect Hook. One important use of these Hooks is to prevent unnecessary re-renders even when nothing changes.

The code below prints a fetched message to the page but doesn’t use a dependency array.

import React, {
    useEffect, useState
} from 'react';
const INITIAL_STATE = '';
const App = () => {
    const [message, setMessage] = useState(INITIAL_STATE);
    useEffect(() => { loadMessage() });
    const loadMessage = () => {
        console.log('>> Loading message <<');
        try {
            fetch('https://json.versant.digital/.netlify/functions/fake-api/message')
                .then(res => res.json())
                .then(message => {
                    setMessage(message);
                });
        } catch (e) { }
    }; 
    console.log(`>> Current message is: ${message || 'EMPTY'} <<`); 
    return <h1>{message}</h1>;
};
export default App;

This seems to be fine, but if when we open the browser console, we can see that the >> Loading Message << was rerun multiple times.

>> Current message is: EMPTY <<
>> Loading message <<
>> Current message is: Master React Hooks! <<
>> Loading message <<
>> Current message is: Master React Hooks! <<

Since the message did not change, we should optimize this to only load and fetch the message once.

The secret is to add an empty dependency array. We simply replace lines 8–10 with:

useEffect(() => {  loadMessage();}, []);

By default, the useEffect Hook runs after each re-render. With a dependency array, it runs once then runs again whenever the passed dependency is changed. An empty array provides no condition where the Hook will run again and therefore ensures that it fetches the message on the first render only.

Run useEffect Function with Change in State or Props

We can also use populated dependency arrays to make responsive apps.

Imagine we have a React app that allows users to set a nickname using an input field. After the nickname is set, it fetches a personalized greeting message from an external API.

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

const App = () => {
  const [message, setMessage] = useState('');
  const [name, setName] = useState('');
  const [isTyping, setIsTyping] = useState(false);

  useEffect(() => {
    // We don't want to fetch message when user is typing
    // Skip effect when isTyping is true
    if (isTyping) {
      return;
    }
    loadMessage(name);
  }, [name, isTyping]);

  const loadMessage = nickName => {
    try {
      fetch(
        `https://json.versant.digital/.netlify/functions/fake-api/message/name/${nickName}`
      )
        .then(res => res.json())
        .then(message => {
          setMessage(message);
        });
    } catch (e) { }
  };

  const handleNameFormSubmit = event => {
    event.preventDefault();
    setIsTyping(false);
  };

  return (
    <div className="App">
      <form onSubmit={handleNameFormSubmit}>
        <input
          value={name}
          onChange={event => {
            setIsTyping(true);
            setName(event.target.value);
          }}
        />
        <button>Set nickname</button>
      </form>
      <h1>{message}</h1>
    </div>
  );
};

export default App;

On lines 8-15, we see that our dependency array contains name and isTyping. The useEffect runs every time there is a change in either of these states. However, you do not want to load the message until the user enters the form or clicks on the “Set nickname” button.

This is achieved with the help of the isTyping state. If isTyping is set, we return from the useEffect function and do not run it (lines 11-13).

When the user finally submits the form, reset isTyping to false. The Hook detects the change in the isTyping state and will run again. It now bypasses the if statement and this time will call the loadMessage function to initiate a fetch request.

You just created a componentDidUpdate method using Hooks!

Read more on Create, Read, Update & Delete application using React Hooks & Context.

Share this if you like it.

Add Comment

  |     |  
Full Stack Developer

About me

A profound honest Full Stack Developer with a passion for creating delicate solutions in the least amount of time, with more than seven years of experience in the Web Application Development. Implemented complex user stories that make the business count on more conversion and grow with it.

Started career with ASP.NET but eventually revamped skills with JavaScript, ReactJs, NodeJs and Ruby on Rails.

Throughout my career, I have contributed to the businesses who have a positive impact on society through my active and innovative contributions to the growth of the organisation. While being an independently motivated engineer, I appreciate the collective efforts and collaborative productivity within the team environment. I am open-minded and focused on learning new technologies which have direct impacts on the betterment of human lives.