Using React Hooks ⚛️

10th February 2021

Trying to learn frontend with React today might be a bit confusing, well it was for me at least. Trying to navigate documentation and Stack Overflow threads doing the same thing in ten different ways. At first I had no idea why that was, but later I realised that it seems like React (and JS) has gone through a few big changes over the years. Once I figured that out, I made sure to learn about how things are done today, using ES7 and React 16.8.

The changes are quite big, and I guess it's a bit of a blessing coming fresh faced into React since it means I don't have to actively try to forget the old ways of doing things in favour of the new.

So I thought I'd write a bit about the main feature that really changes the way you can interact with React, hooks. Note that hooks aren't a compulsory new concept which is about to make everything else that came before it deprecated. But it is an option that's available, and personally I think it's a more clean and intuitive way to work in React (I say this from my relatively limited experience with React!).

What are React Hooks?

React hooks are functions that allow for stateful logic to be introduced into components which can be easily shared between components. Hooks do not work within (and therefore do not require) classes. Instead, we can opt for defining the entire program as function components instead of class components.

Since hooks can be easily passed between components as callback functions, it helps to avoid scenarios of large monolithic class components which end up consisting of large amount of state logic.

With hooks helping us to keep our components small, we can benefit from having better decoupling and as a result, a codebase that is easier to test.

State in React

A good question to ask before writing state logic is where should we introduce it? With components all over the place, what makes one more suited than another to being responsible for initialising the state?

Typically, a top-down approach is used, where any state that is initialised should be done at the top most component that needs it. In other words, any state that is defined in a particular component, should only affect the functionality of its child components. As such, the component can pass its state down as props to its child components. Of course, the same applies for any state initialised in one of the child components.

An example could be of a checkout page on an e-commerce website. A Basket component may be made up of multiple Item child components to represent each item chosen by the user. Since it is ultimately the Basket component's responsibility to keep track of all items, this is where the state for the collection of items can be initialised and stored.

Any other key components on the checkout page such as a Payment or Address component can stay decoupled and store states relevant only to them and their child components.

Have a look at the React docs on state for further explanation.

Stateful logic with useState()

Once we've found a component to introduce state in, all we have to do is call useState(). This returns a both the state variable itself and a setter function for it, both of which can be named as you want. The naming convention is that the variable defined as <state_name>, should have its setter named as set<state_name>.

If needed, the initial state can be passed into useState(). This is especially useful where your state variable needs to be an array or an object instead of a single value.

const [views, setViews] = useState(0);

const [similarSongs, setSimilarSongs] = useState([]);

const [config, setConfig] = useState({});

Once initialised, simply pass the new value to the setter function to update the state. As shown below, we can also have the setter use the previous state in the form of a function if needed. This helps as it means we don't have to pass around the state variable itself for that reason.

As I've mentioned earlier, hooks make it easier to share state between components. To do this, either the variable or the setter can be passed as props to other components. They can then be executed as callback functions in the component that they have been passed to. This ensures that all components sharing the state have a global true view of the state.

Mutable State

Be wary of how the state is updated if it is mutable data such as an array or an object. There may be a situation where a component has some logic that is performed depending on the change of a mutable state (see useEffect below).

Since mutable data is stored as memory locations, an update/insert operation on an object using a state setter will not trigger a new render in React. This is because the memory location of the object would remain the same. Without triggering a new render, the component fails to perform the required logic as it has not detected a change in the state.

To avoid this inconsistency, we can perform deep copies of the previous state into a newly defined state of the same type using the spread operator. Then the state can be updated as required.

// Initialising a state as an object
const [{ username, views }, setProfile] = useState({ username: "johndoe", views: 0 });

// Updating the mutable state by creating a new object with updated values
setProfile(prevState => ({
  ...prevState,
  views: prevState.views + 1
})

Lifecycle logic with useEffect()

The component lifecycle is a key part in how React works, where different events can trigger the execution of some code by a component. It allows for a lot of the dynamic behaviour that's seen across the internet these days.

There are three main events that trigger are:

  1. Mounting: Everytime a component is rendered for the first time

  2. Updating: If a dependency (could be a prop or a state variable) changes

  3. Unmounting: When a component is no longer shown

With just one versatile function, useEffect can be utilised so that we can run desired code on any of these events, or a combination of events. The code below shows how useEffect can be assigned to particular events.

// ON MOUNT
// Note that the dependency array in the second argument is empty, therefore telling useEffect() that it has nothing to be triggered by. This leaves the function to only be triggered when it is mounted.
useEffect(() => {
	// Code to run
}, []);

// ON UPDATE
// This runs when the variable given in the array changes. Multiple variables can be given as dependencies for the function to be triggered by.
useEffect(() => {
	// Code to run
}, [count]);

// ON UNMOUNT
// The return function is executed when the component is removed. This can be used across multiple useEffect functions that require different cleanup procedures.
useEffect(() => {
	// Code to run
	return () => {
		console.log("Removing component");
	};
}, []);

Asynchronous Dependencies

In reality, building a React app will likely require API calls to a backend server or to another service. This introduces asynchronous behaviour in the component that the calls are being made from, bringing a level of complexity in maintaining state and executing lifecycle operations.

I came across a problem with asynchronous state updates when working on component which displays a song and other songs by the same artist. The component is given a single prop, the song ID. Using this, the component would need to make two API calls to the backend:

  1. Getting the details of the song (including artist ID)

  2. Get other songs by that artist (using the obtained artist ID)

The responses for these API calls would be stored in the song and artistSongs states respectively.

There are two challenges here. Firstly, there are multiple API calls to be made by one component. Secondly, and more importantly, the second API call is dependent on the response of first API call.

Due to the asynchronous behaviour that comes with communicating with the backend, it is not guaranteed that the two API calls will get their response and update the associated state sequentially. Thus, it is highly possible that the component may try to get other songs by the artist, even the song state has not yet been populated. This would then produce an error, leading to an incomplete component.

In this case, we can use two useEffect hooks for each of the two API calls required.

The first hook will have no dependencies, resembling a hook to perform "on mount" only. This will get the song details using the song ID prop.

Meanwhile, the second useEffect hook will have a dependency to the song state. This ensures that whenever the song of the component changes, a new call will be made to update artistSongs.

const { songId } = props;
const [song, setSong] = useState();
const [artistSongs, setArtistSongs] = useState([]);

useEffect(() => {
  async function fetchSong() {
    try {
      var res = await axios.get("SONGS_ENDPOINT", {
        params: { song_id: songId },
      });
      setSong(res.data[0]);
    } catch (err) {
      console.log(err);
    }
  }

  fetchSong();
}, []);

useEffect(() => {
  async function fetchSongsByArtist() {
    try {
      var res = await axios.get("SONGS_BY_ARTIST_ENDPOINT", {
        params: { artist_id: song.artistId },
      });
      setArtistSongs([...res.data]);
    } catch (err) {
      console.log(err);
    }
  }

  if (song) {
    fetchSongsByArtist();
  }
}, [song]);

However, even with having song as a dependency in the hook, I was still getting errors when trying to access the artist ID because song had not been populated yet at the time of execution. After looking into it, the issue came from the fact that song was being accessed through JSX within the HTML of the component. This meant the hook was being triggered before the dependency had even been met by the first hook.

To fix this, as shown above, I simply used an if statement to check whether or not the song state had been populated yet. This made sure that even though the hook is called multiple times, it will only attempt to perform the logic if the required dependency has been met.


Long post over!!! 🥵 If anyone's still reading, hope that's of any help to you beginning to figure out React. There's lots more to it that I need to learn but I feel that the concepts of state and logic are two of the most important building blocks to help you get going!