< Articles


Benefits of React Hooks

Right now, React hooks are all the rage. But in conversations I’ve had, some don’t understand the benefits of React hooks. After all, nothing seemed particularly broken with the old way of doing things.

Admittedly, I was in the same boat. Having spent much of my time using classes in React, I had grown to love component lifecycle methods. At first glance, it was difficult to see how React hooks could improve my development experience. However, I decided to give it a try on my most complex Shentaro component, an 850 line behemoth with some complex logic.

In doing so, I quickly realized that react hooks help us collocate related state and logic, write cleaner code, and reuse stateful logic.

Let’s take a look at each of these benefits individually.

Collocation

“Hooks let us collocate these effects, collocate our state, and useReducer lets us collocate the changes to the state in this component.” – Ryan Florence

What is collocation? Collocation is the idea that related code is located together in our applications. Frameworks that focus on components (Angular, React, and Vue) help us in this regard because our component-level code is located within one file or within the same folder.

However, dive a little deeper into a large React component and you’ll quickly realize that related code is often spread throughout our component.

Let’s take a look at a traditional React component using classes.

interface IMouseMovesAndButtonClicksProps {}interface IMouseMovesAndButtonClicksState {
    buttonClicks: number;
    mouseMoves: number;
}export class MouseMovesAndButtonClicks extends React.Component<
    IMouseMovesAndButtonClicksProps,
    IMouseMovesAndButtonClicksState> {
public mouseMoveFunction: () => void;

    public state: IMouseMovesAndButtonClicksState = {
        buttonClicks: 0,
        mouseMoves: 0,
    }

    constructor(props: IMouseMovesAndButtonClicksProps) {
        super(props);
        this.incrementButtonClicks = this.incrementButtonClicks.bind(this);
        this.clearButtonClicks = this.clearButtonClicks.bind(this);
    }

    public componentDidMount(): void {
        this.mouseMoveFunction = () => {
            this.setState({
                mouseMoves: this.state.mouseMoves + 1,
            });
        }
        window.addEventListener('mousemove', this.mouseMoveFunction);

}

    public componentWillUnmount(): void {
        window.clearEventListener('mousemove', this.mouseMoveFunction);
    }

    public incrementButtonClicks(): void {
        this.setState({
            buttonClicks: this.state.buttonClicks + 1,
        });
    }

    public clearButtonClicks(): void {
        this.setState({
            buttonClicks: 0,
        });
    }

    public render() {
        return (
            <div>
                <p>
                    Number of button clicks: {{ this.state.buttonClicks }}
                </p>
                <p>
                    Number of mouse moves: {{ this.state.mouseMoves }}
                </p>
                <button onClick={this.incrementButtonClicks}>Click Here</button>
                <button onClick={this.clearButtonClicks}>Clear Button Clicks</button>
            </div>
        )
    }

}

I want you to notice something from this block. Look at the logic and state pertaining to buttonClicks. It’s spread throughout the file.

For a component this small, it wouldn’t be difficult to move the related logic and state closer together. But how would we handle this in a large component?

Most often, I declare my state at the top, followed by my lifecycle methods, and then any additional logic. Using this pattern, it’s not uncommon to have hundreds of lines between related logic and state.

Now take a look at the equivalent hooks version.

interface IMouseMovesAndButtonClicksProps {}

export function MouseMovesAndButtonClicks(
    props: IMouseMovesAndButtonClicksProps
) {
    const [buttonClicks, setButtonClicks] = useState(0);
    function incrementButtonClicks(): void {
        setButtonClicks(buttonClicks + 1);
    }
    function clearButtonClicks(): void {
        setButtonClicks(0);
    }

    const [mouseMoves, setMouseMoves] = useState(0);
    useEffect(() => {
        const mouseMoveFunction = () => {
            setMouseMoves(mouseMoves + 1);
        };
        window.addEventListener("mousemove", mouseMoveFunction);
        return () => window.clearEventListener("mousemove", mouseMoveFunction);
    }, []);

    return (
        <div>
            <p>Number of button clicks: {{ buttonClicks }}</p>
            <p>Number of mouse moves: {{ mouseMoves }}</p>
            <button onClick={incrementButtonClicks}>Click Here</button>
            <button onClick={clearButtonClicks}>Clear Button Clicks</button>
        </div>
    );
}

Notice how related pieces of state and logic are grouped together. New developers don’t have to jump around to understand slices of our component.

In my option, improved collocation is one of the main benefits to React Hooks.

Cleaner Code

I want you take another look at those two blocks. They do they exact same thing. However, the version using hooks is much shorter and it’s not inundated with binding, references to ‘this’, or a bunch of setState calls. React Hooks tend to produce cleaner code.

Now, that’s not to say that shorter code is always better. I can’t tell you how many times I’ve gone on codewars and laughed at the best answer. It’s nearly unreadable. But if code is equally understandable between the two blocks, I’d say the shorter syntax is a big win.

Reusable Stateful Logic

Aside from cleaner code, it’s also significant that we can now reuse stateful logic between components.

Let’s take a look at a tweaked version of Ryan’s example from React conf. For this, we’ll create a custom hook that subscribes to the screen width.

function useMedia(query) {
    const [matches, setMatches] = useState(window.matchMedia(query).matches);

    useEffect(() => {
        const media = window.matchMedia(query);
        if (media.matches !== matches) {
            setMatches(media.matches);
        }

        const listener = () => setMatches(media.matches);
        media.addListener(listener);
        return () => media.removeListener(listener);
    }, [query]);

    return matches;
}

This custom hook takes in a query and gives us back a boolean that indicates whether or not the window matches the current query. Additionally, we have an effect that will update our ‘matches’ state anytime the device width changes.

Now that we’ve created a custom hook, we can use it in multiple components.

Here, we use it to create a responsive nav bar.

function ResponsiveNavBar() {
    const small = useMedia("(max-width: 400px)");

    return small ? <HamburgerNav /> : <Nav />;
}

And here, we use it create a responsive blog post excerpt.

interface IResponsiveBlogExcerptProps {
    postText: string;
}
function ResponsiveBlogExcerpt(props: IResponsiveBlogExcerptProps) {
    const small = useMedia("(max-width: 400px)");
    const excerptLength = small ? 90 : 180;
    const correctLengthExcerpt = props.postText.slice(0, excerptLength);
    return <p>{correctLengthExcerpt}</p>;
}

With functions as the base react primitive, we can create reusable hooks to share stateful logic between components.

Conclusion

React hooks are a powerful way to collocate your code, write cleaner components, and reuse stateful logic across your app. It’s a big leap forward in app development and represents the future of React.