React Hooks: The Missing Link to Efficient and Scalable Frontend Architecture

As a software developer your most important asset is time hence before you spend time learning a new technology you need to understand the why it was introduced and what problem it solves.

In this post I’ll be discussing the reason why React Hooks was such a big and fundamental addition to React.

A brief history on additions made to React and why they were introduced is important to set the context:

  • React was introduced to solve the issues with imperative programming which was time consuming, high likelihood of having an application in an inconsistent state and generally making the development of an application not scalable – it solved these issues by making development declarative and introducing a component based architecture.

  • Flux state management was introduced to solve prop drilling

  • Redux was introduced to solved Flux multiple stores by introducing one store

  • And Hooks introduced to solve the tight coupling issue in our components introduced by the component lifecycles which made sharing non visual logic non obvious and convoluted – sharing logic, up to the introduction of hooks was solved by 2 not so obvious patterns:

     a) Render Props pattern
    
     b) Higher order components (HOC)
    

In this post we’ll be looking specifically at the HOC way of handling the sharing of non visual logic.

Sharing logic

To illustrate how React hooks solve the sharing of non visual logic and by so doing massively improving the reusability of our components, let’s consider the following simple example:

				
					// App.tsx
// class component with window resize logic that you may want to use in another component
class App extends Component {
  state = {
    width: 0,
    height: 0,
  };

  handleWindowResize = () => {
    this.setState({
      width: window.innerWidth,
      height: window.innerHeight,
    });
  };

  componentDidMount() {
    if (typeof window !== "undefined") {
      window.addEventListener("resize", this.handleWindowResize);
      this.setState({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
  }

  componentWillUnmount() {
    window.removeEventListener("resize", this.handleWindowResize);
  }

  render() {
    const { width, height } = this.state;
    return (
      <main>
        <h1>App</h1>
        <section>
          <p>Width: {width}</p>
          <p>Height: {height}</p>
          <div>
            <Link to="/about">Go to About Page</Link>
          </div>
        </section>
      </main>
    );
  }
}

export default App;




// About.tsx
//To have the logic in App.tsx also in this component we would have to repeat the code

import React, { Component } from "react";
import { Link } from "react-router-dom";

class About extends Component {
	render() {
		return (
			<div>
				<h1>About page</h1>
				<div>
					<Link to="/">HomePage</Link>
				</div>
			</div>
		);
	}
}

export default About;
				
			

Defining the Problem:

In the App.tsx we have implemented the functionality to track window size.

But what if we want to use the window size in another component? We could do that through props, assuming the second component is a child of the component we’ve implemented the logic in.

But what if the second component was a deeply nested component? And worse what if the second component was not at all a child of the component with the logic? In that case we’d need a way to extrapolate the logic and make it reusable.

As our application grows it is likely that functionalities that we might want to later on share increase in number as well so having a way to share them without passing them through props and thus avoid prop drilling will make our code more resilient and less spaghetti.

Solution: Sharing the code - Implementing HOC

To share the functionality implemented in App.tsx with About.tsx we’ll need to create a wrapper component that encapsulates the logic and wrap that component around App.tsx and About.tsx

 

				
					
// withWindowResize.tsx
// HOC

const withWindowResize = (Component) => {
	return class ComponentWithWindowResize extends React.Component {
		state = {
			width: 0,
			height: 0,
		};

		handleWindowResize = () => {
			this.setState({
				width: window.innerWidth,
				height: window.innerHeight,
			});
		};

		componentDidMount() {
			if (typeof window !== "undefined") {
				window.addEventListener("resize", this.handleWindowResize);
				this.setState({
					width: window.innerWidth,
					height: window.innerHeight,
				});
			}
		}

		componentWillUnmount() {
			window.removeEventListener("resize", this.handleWindowResize);
		}

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

export default withWindowResize;

				
			
				
					// sharing the logic with the now updated App.tsx

import { Link } from "react-router-dom";
// importing the HOC
import withWindowResize from "./withWindowResize.tsx"


class App extends Component {
	render() {
		const { width, height } = this.props;
		return (
			<main>
				<h1>App!!</h1>
				<section>
					<p>Width: {width}</p>
					<p>Height: {height}</p>
					<div>
						<Link to="/about">Go to About Page</Link>
					</div>
				</section>
			</main>
		);
	}
}

// using the HOC
export default withWindowResize(App);
				
			

The above code works; but what if we want to share multiple non visual functionalities? It’d get cumbersome very easily.

And this is why Hooks were introduced.

Implementation with hooks:

				
					// useWindowSize.ts
// track window resize functionality implemented with a custom hook
const useWindowResize = () => {
	const [width, setWidth] = useState(0);
	const [height, setHeight] = useState(0);

	const handleWindowResize = () => {
		setWidth(window.innerWidth);
		setHeight(window.innerHeight);
	};

	useEffect(() => {
		window.addEventListener("resize", handleWindowResize);
		setWidth(window.innerWidth);
		setHeight(window.innerHeight);

		// cleanup for when component unmounts - componentWillUnmount
		return () => {
			window.removeEventListener("resize", handleWindowResize);
		};
	}, []);

	return { width, height };
};

export default useWindowResize;
				
			

Now to use it all we have to do is to go to the component in which we want to use the functionality and do as follows:

				
					// App.tsx
// We had to refactor App.tsx as hooks can only be used in functional component

import useWindowResize from "./useWindowResize";

const App = () => {
  // using the hook
	const { width, height } = useWindowResize();
	
	return (
		<main>
			<h1>App!!</h1>
			<section>
				<p>Width: {width}</p>
				<p>Height: {height}</p>
				<div>
					<Link to="/about">Go to About Page</Link>
				</div>
			</section>
		</main>
	);
};

				
			

And there you have it!

As you can see from the code above, the hooks way of creating reusable logic is more straightforward and intuitive ensuring a higher quality in our code.

In conclusion, React hooks solves the wrapper hell workaround of HOC to share application logic making our components truly reusable and our applications more robust and reliable.