Pallalax Section

最近では好んで実装されていないかも？Pallax Section です。視覚効果以外に「画面内判定トリガー」が後日サンプルの役に立つので紹介します。まずは一番簡単な toggle class で実現する pallax 視覚効果で仕組みを理解します。
code: [github](https://github.com/takefumi-yoshii/react-hooks-ogiri/tree/master/apps/pallalax-section) / `$ yarn 1207`

|![1207.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/182ca0d9-1e7c-fe1e-11b5-3ce9d1adca2e.jpeg)|![1207-1.jpg](https://qiita-image-store.s3.amazonaws.com/0/59071/bcc0c2ae-6a11-1cd4-9351-ef7b030f85ad.jpeg)|
|---|---|

以下の様に、wrapper 内部は動きのロジックに関与することなく、自由にコンテンツを配備出来る様に実装します。

```components/sections/section1.tsx
const View = (props: Props) => (
  <PallalaxSection
    className={props.className}
    toggleClassName={'inArea'}
    topThrethold={100}
  >
    <img src={require('./assets/1.jpg')} width="100%" />
    <h2>Rabbit</h2>
    <p>
      Lorem ipsum dolor sit amet consectetur adipisicing
      elit. Iste reiciendis voluptatum, cum in nulla dolore
      fugiat quis et id. Sunt minima culpa atque qui velit
      aliquid facilis, vero error ad!
    </p>
  </PallalaxSection>
)
```

# Component 概要図

Wrapper は包含要素が画面内に入っているか否かを保持しています。画面内ににセクションが入ったかどうかの判定は、セクションの上辺と下辺の座標から算出します。この時、座標ピッタリで class を toggle してしまうと、動いている様子があまり見えません。任意の閾値分だけ、画面内判定の距離を狭くします。topThrethold と bottomThrethold がその設定値です。

![1207.png](https://qiita-image-store.s3.amazonaws.com/0/59071/31d91c31-1b22-2f5a-5f5c-75ec38cddfea.png)


# usePallalaxSection

今回定義した Custom Hooks です。画面判定処理が混み入っているだけで、他は単純です。

```components/sections/usePallalaxSection.ts
const usePallalaxSection = (props: Props) => {
  const [state, setCurrent] = useState<State>(...)
  const options = useMemo(...)
  useEffect(...) // 画面内判定
  useEffect(...) // 判定後処理
}
```

以下の State を保持しています。

```components/sections/usePallalaxSection.ts
type State = 'OUTSIDE' | 'IN_AREA' | null
```

### 画面内判定

画面内判定ロジックです。負荷を軽減するために、[lodash.throttle](https://www.npmjs.com/package/lodash.throttle) でスクロール処理を間引きします。この throttleInterval も Optional Injection で調整出来る様にしておきます。

```components/sections/usePallalaxSection.ts
useEffect(
  () => {
    const handleWindowScroll = throttle(() => {
      if (props.ref.current === null) return
      const {
        top,
        height
      } = props.ref.current.getBoundingClientRect()
      const topThrethold = options.topThrethold
      const bottomThrethold = options.bottomThrethold
      const offsetBottom = window.innerHeight - top
      const offsetTop = top + height
      const isOutOfBottom = offsetBottom < 0
      const isOutOfTop = offsetTop < 0
      const isInArea =
        !isOutOfBottom &&
        !isOutOfTop &&
        offsetBottom > bottomThrethold &&
        offsetTop > topThrethold
      if (isInArea) {
        setCurrent('IN_AREA')
      } else {
        setCurrent('OUTSIDE')
      }
    }, options.throttleInterval) // here
    handleWindowScroll()
    window.addEventListener('scroll', handleWindowScroll)
    return () =>
      window.removeEventListener(
        'scroll',
        handleWindowScroll
      )
  },
  [
    props.ref,
    options.topThrethold,
    options.bottomThrethold,
    options.throttleInterval
  ]
)
```

### 判定後処理

このサンプルでは、出入りの度に toggle class していますが、判定後処理を props callback に委ねることも出来ます。

```components/sections/usePallalaxSection.ts
useEffect(
  () => {
    if (props.ref.current === null) return
    if (state === 'IN_AREA') {
      props.ref.current.classList.add(
        props.toggleClassName
      )
    } else {
      props.ref.current.classList.remove(
        props.toggleClassName
      )
    }
  },
  [props.ref, state]
)
```

# Wrapper Component へ適用

冒頭の画面内判定閾値などを Optional Injection で注入します。この様に Wrapper とコンテンツを分離することで rerender を抑止します。

```components/sections/pallalaxSection.tsx
export default (props: Props) => {
  const ref = useRef({} as HTMLDivElement)
  usePallalaxSection({
    ref,
    toggleClassName: props.toggleClassName,
    topThrethold: props.topThrethold, // here
    bottomThrethold: props.bottomThrethold, // here
    throttleInterval: props.throttleInterval
  })
  return (
    <section ref={ref} className={props.className}>
      {props.children}
    </section>
  )
}
```