DEV Community

Lori Baumgartner
Lori Baumgartner

Posted on

My First Custom Hook in React

If you want to follow along, here's the codesandbox with hooks:


I've been slow the React Hooks game. First it was because my last company was on an older version of React and lately it's mostly been I just haven't focused on learning them and adding them to my code.

It seems obvious to me that hooks are here to stay, so I've recently been doing some reading and felt ready to jump into my codebase to practice.

I read a bit about how hooks were potentially good replacements for higher order components (HOC). I recently created an HOC that was checking for window resizing and communicating whether the window size met our "mobile" screen width of 640 pixels or less.

That component looked like this to start:

// connectResizer.js

import React, { Component } from 'react'

export default function withResizer(WrappedComponent) {
  return class ResizeHandler extends Component {
    constructor(props) {
      super(props)
      this.state = {
        isMobile: window.innerWidth < 640,
      }
    }

    componentDidMount() {
      window.addEventListener('resize', this.resizeWindow)
      this.resizeWindow()
    }

    componentWillUnmount() {
      window.removeEventListener('resize', this.resizeWindow)
    }

    resizeWindow = () => {
      this.setState({ isMobile: window.innerWidth < 640 })
    }

    render() {
      return <WrappedComponent isMobile={this.state.isMobile} {...this.props} />
    }
  }
}

Honestly, it works just as we needed. It passed an isMobile boolean prop to its wrapped component and we could go on our merry way implementing conditional logic like this:

// components/Navbar.js

function Navbar({ isMobile, org, user, baseUrl }) {
  if (isMobile) {
    return (
      <>
        <Dropdown>
          <AccountLinks isMobile={isMobile} baseUrl={baseUrl} />
        </Dropdown>
        <CartLink
          user={user}
          org={org}
          isMobile={isMobile}
        />
      </>
    )
  }

  return (
    <>
      <AccountLinks isMobile={isMobile} />
      <CartLink
        user={user}
        org={org}
        isMobile={isMobile}
      />
    </>
  )
}

export default withResizer(Navbar) // wrap that component to get access to isMobile in Navbar

But it's also a really great example of something that can be replaced with a useEffect hook:

  • it is using multiple React LifeCycle methods
  • it has some internal state that needs to be communicated to and reused by other components
  • it's pretty straightforward and easy to test

Just a note that the following example is in TypeScript because we are currently migrating our codebase over to TypeScript and if I were to change this component, I would be rewriting it in TypeScript.

So, here's what the final hook function looks like:

// useResizer.ts

import * as React from 'react'
export default function useResizer(): boolean {
  const [isMobile, setIsMobile] = React.useState(window.innerWidth < 640);

  function handleSizeChange(): void {
    return setIsMobile(window.innerWidth < 640);
  }

  React.useEffect(() => {
    window.addEventListener("resize", handleSizeChange);

    return () => {
      window.removeEventListener("resize", handleSizeChange);
    };
  }, [isMobile]);

  return isMobile;
}

It is definitely less lines of code than our HOC. But is it more readable? Because hooks are still new to me, I'm not sure. But let's dive in to see what's going on.

  // useResizer.ts

  const [isMobile, setIsMobile] = React.useState(window.innerWidth < 640);

This one line using the useState hook gives us:

  • our state value of isMobile,
  • a setter setIsMobile that will take a value and update state to that given value,
  • and a default value window.innerWidth < 640.

We'll be calling that method to actually update our state when our hook is notified of changes to the window width.

  // useResizer.ts

  function handleSizeChange() {
    return setIsMobile(window.innerWidth < 640);
  }

Next is our callback we pass to our window event listeners. You can see this is using our useState helper to set the isMobile boolean value when handleSizeChange is called.

Now the fun part 🙌

  // useResizer.ts

  React.useEffect(() => {
    // add event listener - update our local isMobile state
    window.addEventListener("resize", handleSizeChange);

    // handle cleanup - remove event listener when effect is done
    return () => {
      window.removeEventListener("resize", handleSizeChange);
    };
  }, [isMobile]); // add dependency - only use our effect when this value changes

Finally, don't forget this uber important last line that is outside of our useEffect function:

// useResizer.ts

return isMobile;

This is the bit that is returning the actual value of isMobile and making it accessible to the components consuming useResizer().

At the end of the day, we would update the example above to look like this:

// components/Navbar.js

function Navbar({ org, user, baseUrl }) { // notice isMobile is gone from props
  const isMobile = useResizer() // because now we use our hook!
  if (isMobile) {
    return (
      <>
        <Dropdown>
          <AccountLinks isMobile={isMobile} baseUrl={baseUrl} />
        </Dropdown>
        <CartLink
          user={user}
          org={org}
          isMobile={isMobile}
        />
      </>
    )
  }

  return (
    <>
      <AccountLinks isMobile={isMobile} />
      <CartLink
        user={user}
        org={org}
        isMobile={isMobile}
      />
    </>
  )
}

export default Navbar // no more HOC wrapper needed here, either!

Well, that's it. What do you think? I still have a lot to learn (including the gotchas) but it's starting to make sense to me.

Are you and your teams all-in on hooks or holding tight to class components?

Latest comments (1)

Collapse
 
jacobedawson profile image
Jacob E. Dawson • Edited

Hi Lori,

Nice post! I'm getting into hooks as well atm.

One question: is there a reason that you put 'isMobile' in the deps array of the useEffect hook? As far as I understand, if you add an empty array then useEffect will only run on mount & unmount, whereas in this case as the page is resized the boolean value of 'isMobile' will change and run useEffect again each time. It won't duplicate the event listener but seems unnecessary, unless I'm missing something?

Cheers,

Jake