React-Hooks first impressions

This article will outline the benefits and quirks I’ve discovered building a small side-project using React-Hooks. The main observations I will discuss are React-Hooks effects on readability, cognitive load, and testing. I will also discuss some quirks and pain points that I experienced


Hooks are an upcoming feature that lets you use state and other React features without writing a class. They’re currently in React v16.7.0-alpha.

Example Code

Below is some brief sample code. If you are already familiar with Hooks, feel free to skip this section. If you want to see more in depth usage examples, you can visit the Reactjs.org website:

import { useState, useEffect } from 'react';

function Example() {
const [count, setCount] = useState(0);

useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes

return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

Setting State

In the above code count is the state variable and setCount is a function that updates the count value. In other words, setCount(12) is equivalent to this.setState({count: 12}). The 0 argument passed to useStatetells the component that 0 is the default variable for count.

Effects (replaces life-cycle functions)

useEffect will run every time the componentDidMount and componentDidUpdate life-cycle functions would run. By adding an array argument containing a reference to the state variable, [count]. We are explicitly telling the effect to run only when the count state variable value changes.


I see two major benefits from using this new syntax:

Benefit #1: Individual functions for setting each state variables improves readability

When skimming a large React class, it can quickly become difficult to know when and where different state variables are updated. Especially when setState can set multiple state variables at the same time. For example if your React class contained one function that called setState{{user: 3, profileClicked: true}) and another that called setState({displayName: "john", profileClicked: false}). It is difficult to find everywhere in the component that profileClicked can be updated. Even worse, the state variables can be variables that are declared earlier in amethod. This makes deciphering state changes difficult.

By having an individual method for updating a particular state variable, we are able to separate concerns and reduce the cognitive load necessary to understand the component’s function.

Benefit #2: Encourages small, single function components

Even the best developers can be seduced into slowly creating a giant component class. It starts with adding a function here, a function there, two more state variables to go with the two functions, a few months later, and all of sudden we have massive 500 line components that are difficult to read and reuse.

By explicitly declaring the state with a hook, when a component starts to bloat it produces a strong code smell. A component with 10+ lines of state variable declarations or 10+ effects is probably handling more functionality that it should.

Benefit #3: Prevents common life-cycle bugs

The React team did a great job explaining this in their documentation. When event subscriptions happen inside components that require prop or state variables. It is easy to forget to re-subscribe to the subscription when the required prop or state variable changes. This leads to memory leaks and UI bugs that are not readily apparent. Here is an example from the Hooks docs where componentDidUpdate is necessary but often forgotten:

componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}

componentDidUpdate(prevProps) {
// Unsubscribe from the previous friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe to the next friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}

componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
  }

With Hook’s useEffect method we can replace the above code with:

useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

A few pain points were also encountered when using hooks. Keep in mind Hooks are brand new and these pain points may very well be solved by the time I finish this article.

Pain Point #1: Declaring methods that should run once is wonky

In the app we needed a way to trigger analytics data on the initial component mount only once. We could do this with a single declaration of componentDidMount in a React class and calling the runThisOnce.

The documentation currently suggests that an empty array can be used to run an effect once. useEffect(() => {...}, []), but it should “not become a habit” because it can “lead to bugs”. A more explicit way to do this would have to set a state variable to explicitly track whether the effect has been called. Like this:

import { useState, useEffect } from 'react';

function Example() {
const [loaded, setLoaded] = useState(false);

useEffect(() => {
setLoaded(true)
runThisOnce()
}, [loaded]);

return (
// ...
);
}

The first solution is implicit but readable, the second solution is explicit but bloated.

Pain Point #2: Unit tests can become brittle and bloated

When testing hooks you may want to test if and how particular state setting methods are called. To perform these tests, you will need to mock out the useStatemethod with multiple return values to account for multiple state declarations. Because the ordering is important, a change in the order of state declaration in the component will break all the unit tests for that component. Take the following for example:

import { useState, useEffect } from 'react';

function Example() {
const [count, setCount] = useState(0);
const [name, setName] = useState("John");
const [age, setAge] = useState(27)

return (
// ...
);
}

Using jest your mock function for the useState method would look like this:

const setCountMock = jest.fn()
const setNameMock = jest.fn()
const setAgeMock = jest.fn()
const useStateMock = jest.fn()
.mockReturnValue([0, setCountMock])
.mockReturnValue(["John", setNameMock])
.mockReturnValue([27, setAgeMock])
// Later on in a test for count being expect(setCountMock).toBeCalledWith(arg1);

You can see in this circumstance mockReturnValue is dependent on the order of their declaration. Therefore, if we change the order of the useState declarations in the Example component, the tests will break. So when you want to reorganize variables, or add new ones you have to rewrite your tests. Not to mention mocking out every state setting function can cause your tests to become rather long.


I believe hooks is a step forward in a positive direction and the immediate benefits of explicitness, readability, and encouragement of good habits out weigh any pain points.