Let's Use a Hiding Header Hook
Tags: typescript, react, tutorial, webdev
Published: 2020-06-11 18:29:59

Recently our team at the [PWA Store](https://progressapp.store) decided to upgrade our header to a **hiding header**. A hiding header allows for more screen space on mobile and over all makes our react app feel more responsive. This is a huge bonus if your app has a lot of data to show, but minimal space to show it. 

# Third Party Solutions

The [**PWA Store**](https://progressapp.store) was created with [React Ionic](https://ionicframework.com/docs/react). Ionic does a lot of the heavy lifting for the developer. And wouldn't you know it, their own [documentation](https://ionicframework.com/docs/react/quickstart) already has a hiding header on it. So adding that functionality should be _ez pz_, right? Wrong.

After diving deep into the [header](https://ionicframework.com/docs/api/header) component documentation it was clear that hiding the header was not possible through Ionic. There is a function called [`collapse`](https://ionicframework.com/docs/api/header#properties), but this only works on iOS. Also, it just hides to reveal another smaller header. 

Our second thought was to search npm for something that was already implemented. We ran across [React Headroom](https://kyleamathews.github.io/react-headroom/) and it seemed to be everything we were looking for and just an `npm install` away.

After installing and adding it to our app, Headroom was broken. It didn't work nicely with our app. Actually it didn't work at all. Bummer.

# Build a `HidingHeader` Component

At this point we realized it was time to create it on our own. Since the header would be on many of the app listing pages it would need to be reusable. The first idea was to make a `HidingHeader` component. The logic for checking the scroll distance of the `content` would reside inside the component making adding the header to a page a simple import.

```ts
<HidingHeader scrollY={scrollY} />
```

But this created too many unnecessary rerenders to the [DOM](https://www.w3schools.com/js/js_htmldom.asp) as every change in the `scroll y position` of the `content` was causing the `HidingHeader` component to rerender. The only time that the `HidingHeader` needs to update is when its position should change. So how do we hold that state and only update the header when it is actually needed?

# Introducing the `useHidingHeader` Hook :clap::clap::clap:

```ts
const [hideDecimal, setScrollY] = useHidingHeader(threshold: number)
```

`useHidingHeader` [hook](https://reactjs.org/docs/hooks-intro.html) updates a decimal value called `hideDecimal` between 0-1 to let the `HidingHeader` component know how much of the header should be hidden. 0 means not hidden at all and 1 fully hidden. Our page's `content` component sends a callback when scrolling in the y direction updates. This value is then set in the `setScrollY(position: number)` state. Finally we pass a `threshold` value into the hook to tell it how much of a change in scroll it takes to completely hide the header. Handling the state of the Header this way ensures that the `HidingHeader` component will not update for state change unless there is an actual change in how it is displayed.

## `HidingHeader.tsx`

```ts
import { IonHeader, IonToolbar } from "@ionic/react"
import React, { memo, useMemo, useRef } from "react"

interface ContainerProps {
  children: any
  // number between 0 and 1
  hideDecimal: number
}

const HidingHeader: React.FC<ContainerProps> = ({ hideDecimal, children }) => {
  const header = useRef<any>(null)
  const styles = useMemo(
    () => ({
      marginTop: `${-hideDecimal * 100}px`,
      marginBottom: `${hideDecimal * 100}px`,
    }),
    [hideDecimal]
  )

  return useMemo(
    () => (
      <IonHeader
        ref={header}
        style={styles}
        className="ion-no-border bottom-line-border"
      >
        <IonToolbar>{children}</IonToolbar>
      </IonHeader>
    ),
    [children, styles]
  )
}

export default memo(HidingHeader)
```  

We update the margins of our Header component when the `hideDecimal` changes. This moves the Header up and away from view in the window. 

## `useHidingHeader.ts`

```ts
import { useState, useEffect } from "react"

type NumberDispatchType = (
  threshold: number
) => [number, React.Dispatch<React.SetStateAction<number>>]

export const useHidingHeader: NumberDispatchType = (threshold: number) => {
  const [initialChange, setInitialChange] = useState<number>(0)
  const [scrollYCurrent, setScrollYCurrent] = useState<number>(0)
  // number between 0 and 1
  const [hideDecimal, setHideDecimal] = useState<number>(0)
  const [scrollYPrevious, setScrollYPrevious] = useState<number>(0)

  useEffect(() => {
    // at the top or scrolled backwards => reset
    if (scrollYCurrent <= 0 || scrollYPrevious > scrollYCurrent) {
      setHideDecimal(0)
      setInitialChange(scrollYCurrent)
    } else {
      if (scrollYCurrent > initialChange) {
        // start hiding
        if (scrollYCurrent < initialChange + threshold)
          setHideDecimal((scrollYCurrent - initialChange) / threshold)
        // fulling hidden
        else if (hideDecimal !== 1) setHideDecimal(1)
      }
    }
    setScrollYPrevious(scrollYCurrent)
  }, [scrollYCurrent])

  return [hideDecimal, setScrollYCurrent]
}
```

### Typing the Hook

```ts
type NumberDispatchType = (
  threshold: number
) => [number, React.Dispatch<React.SetStateAction<number>>]
```

One of the most annoying, but rewarding parts of using Typescript is typing your objects. So in this instance, how do you type a hook? First we must understand what our hook really is. 

`useHidingHeader` takes in a number and returns an array. The array's order is important, so we must take that into consideration when typing. Inside our array we have a `number` and the setter. The setter is a dispatch function defined inside the body of our hook. This _setter_ is actually a React Dispatch that **dispatches** an action for setting a `useState`'s value.

### The Logic

```ts
// at the top or scrolled backwards => reset
if (scrollYCurrent <= 0 || scrollYPrevious > scrollYCurrent) {
  setHideDecimal(0)
  setInitialChange(scrollYCurrent)
} else {
  if (scrollYCurrent > initialChange) {
    // start hiding
    if (scrollYCurrent < initialChange + threshold)
      setHideDecimal((scrollYCurrent - initialChange) / threshold)
    // fulling hidden
    else if (hideDecimal !== 1) setHideDecimal(1)
  }
}
setScrollYPrevious(scrollYCurrent)
```

The actual logic behind the hook can be found within the `useEffect`. We must store the `initialChange` value of the scroll. This is the value that the scroll y is compared to. Next, we need to store the `scrollYPrevious` value of the scroll. This is the value that the scroll bar was at the previous time the scroll was updated. 

Every time `scrollYCurrent` is set we execute the function in the `useEffect`. 

If the scroll bar is at the top or its value is less than the previous value we reset the header's position by updating `hideDecimal` to 0. 

When scrolling down two things can happen: we are in between the `initialChange` value and the `threshold` or we have passed that state and are continuing to scroll down.

## Usage

```ts
const Home: React.FC = () => {
  const [hideDecimal, setScrollYCurrent] = useHidingHeader(50)
  return (
    <IonPage>
      <HidingHeader hideDecimal={hideDecimal}>
        <div className="HomeHeader">
          <div>
            <h1>PWA Store</h1>
            <IonNote>Progressive Web App Discovery</IonNote>
          </div>
        </div>
      </HidingHeader>
      <IonContent
        fullscreen={true}
        scrollEvents={true}
        onIonScroll={(e) => setScrollYCurrent(e.detail.scrollTop)}
      >
        <div>
          Things and stuff.
        </div>
      </IonContent>
    </IonPage>
  )
}
```

# Wrapping Up

When some state changes every frame, it can be very beneficial to update the side effects to that change only when necessary. This limits the amount of rerenders to the DOM and our application's overall performance. By using a hook to control the state of our header's margins, we are able to update our header only when it really matters. 

Here we see the DOM update only happening when the header is changing its size.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/dgrfxcx9ko8ys7wyrapj.gif)

Thanks for reading and please let me know if you can come up with an even better way to do this!

# :v: