DEV Community

loading...
Cover image for React hooks on steroids

React hooks on steroids

Pathetic Geek
I can see bugs in your code but not my crush's hints
・4 min read

Introduction

This isn't going to be just another hooks and context tutorial, this is going to be me writing about how to do react hooks and state management like a pro. And, it can be a little too much to digest, so grab your favourite snack and jump in.
This will be a series of three posts which will take your react hook and state skills as high as I am while writing this. If you prefer to read it in long form here's the link

Hold on, if you don't know the basics of react hooks and react context API, I highly recommend learning about them first.

1. Setting the stage with hooks

So, we've been using react's new functional components and hooks for a while now, but how many of you have realized the actual power of hooks?

First, we will look at some places where a custom hook might be good and how we implement one.

1.1 A basic useDarkMode hook

So we're coders we love dark themes, but not everyone does, so we need to have some theme state in our app.
We will use the window.matchMedia to match a CSS media query which is prefers-color-scheme: dark. This will tell us if the user's system theme is dark or not, and this will be our initial state.

const matchDark = '(prefers-color-scheme: dark)'

const useDarkMode = () => {
  const [isDark, setIsDark] = useState(() => {
    if (process.browser) {
      return window.matchMedia && window.matchMedia(matchDark).matches
    }
    return false
  })

  return isDark
}

export default useDarkMode
Enter fullscreen mode Exit fullscreen mode

1.2 Making useDarkMode actually useful

Now some people man… they just can't decide if they want light or dark theme, so they put it on auto. And now, we have to account for THAT in our applications.
How we do that is, we can attach a listener to window.matchMedia and listen for when it changes.
Now to do that in code…

const matchDark = '(prefers-color-scheme: dark)'

const useDarkMode = () => {
  const [isDark, setIsDark] = useState(() => {
    if (process.browser) {
      return window.matchMedia && window.matchMedia(matchDark).matches
    }
    return false
  })

  useEffect(() => {
    const matcher = window.matchMedia(matchDark)
    const onChange = ({ matches }: MediaQueryListEvent) => setIsDark(matches)
    matcher.addListener(onChange)
    return () => {
      matcher.removeListener(onChange)
    }
  }, [setIsDark])

  return isDark
}

export default useDarkMode
Enter fullscreen mode Exit fullscreen mode

And now how will be using this hook will look something like

import useDarkMode from "@hooks/useDarkMode";

const App = () => {
    const theme = useDarkMode() ? themes.dark : themes.light;

    return (
        <ThemeProvider value={theme}>
            ...
        </ThemeProvider>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now pat yourself on the back! You've made a useful custom hook.

1.3 The hook most needed useInView

One more common thing we often need is some way to detect if an element is in view or not. Here, most of us would find ourselves reaching for a library to do this but this is way simpler than it seems.

How to do this is simple:

  1. We listen for scroll on window
  2. We get the bounding client rect of our element to get it's offset from top
  3. We check if (offset of element from top + height of element) is > 0 and if the offset from top of element is < window's height, if both are true then our element is visible.
  4. If state is not correct then we set the state and call the onChange function if present.
const useInView = (
  elRef: MutableRefObject<HTMLElement | null>,
  onChange?: (_inView: boolean) => void
) => {
  const [inView, setInView] = useState(false)

  useEffect(() => {
    const onScroll = () => {
      if (!elRef.current) return

      const boundingRect = elRef.current.getBoundingClientRect()
      const elementHeight = elRef.current.offsetHeight
      const offsetTop = boundingRect.top
      const windowHeight = window.innerHeight
      const isVisible =
        offsetTop + elementHeight > 0 && offsetTop < windowHeight
      if (isVisible && !inView) {
        setInView(isVisible)
        onChange && onChange(isVisible)
      } else if (!isVisible && inView) {
        setInView(isVisible)
        onChange && onChange(isVisible)
      }
    }

    window.addEventListener('scroll', onScroll)
    return () => {
      window.removeEventListener('scroll', onScroll)
    }
  }, [elRef, onChange, inView])

  return inView
}
Enter fullscreen mode Exit fullscreen mode

Using this hook is as simple as creating it

import React, { useRef } from 'react'

import useInView from '@hooks/useInView'

const Hooks = () => {
  const elementRef = useRef<HTMLDivElement>(null)
    // use as a variable
    const inView = useInView(elementRef)
    // or use a callback
    useInView(elementRef, (isInView) => {
        console.log(isInView ? 'element has appeared' : 'element has disappeared');
    })

  return (
    <div className="w-full max-w-screen-md">
      <div className="h-screen"></div>
      <div
        ref={elementRef}
        className={`py-6 text-center ${
          inView ? 'bg-blue-100' : 'bg-red-100'
        }`}>
        Is in view: {inView ? 'true' : 'false'}
      </div>
      <div className="h-screen"></div>
    </div>
  )
}

export default Hooks
Enter fullscreen mode Exit fullscreen mode

And now you can probably imagine all the places hooks can be useful. In the next part, we'll look at how to manage state in react apps without losing your sanity.

Discussion (4)

Collapse
loopmode profile image
Jovica Aleksic • Edited

Nice indeed, but you should note that getBoundingClientRect causes a reflow in the browser. Use the hook on a couple hundred of elements or more, and the performance will quickly suffer on lower-tier devices :)
There is a reason why there are specialized libraries after all.. however for a bunch of elements or even 'that one that needs it" it's a good solution tho. Nice writeup overall!

Collapse
patheticgeek profile image
Pathetic Geek Author

Yeah I'm not that pro yet and haven't run into any issues using it so didn't know that performance might be that bad. IntersectionObserver can be used here if someone whats to use this in prod.

Collapse
shubhcode profile image
shubhcode

Nice

Collapse
hellosagar profile image
Sagar Khurana

Nice!