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?
Top comments (1)
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