Reactでスクロール位置を表示してみる

## はじめに
スクロール位置をとって表示するみたいなことをしようと思ったときに、ググっても出てくるのがjQueryの情報ばかりで困ったので、Reactでやってみました。

## 何を作るか
縦にスクロールしたときに、縦位置がなんなのかを表示するコンポーネントを書いてみます。
できあがるのは↓こんなやつです。
![demo.gif](https://qiita-image-store.s3.amazonaws.com/0/143534/495b96d8-a2ff-92a3-d658-d4b100bb3e09.gif)

## 作ってみる
基本的な方針は、コンポーネントの状態としてスクロール量を保持し、状態をupdateするための関数をコンポーネントのマウント時にEventLilstenerとして登録するみたいな感じです。順番にコードを追加していきます。

### まずはコンポーネントを作ってみる
なにはともあれ、ステートレスなコンポーネントを作ってみます。

```tsx
import * as React from 'react';

type Props = {
    scroll: number;
};

const component: React.SFC<Props> = (props: Props) => {
    return (
        <div>
            Scroll: {props.scroll.toString()}
        </div>
    );
};
```

簡単ですね。実際に`scroll`をPropsとしてうけとるだけです。
これだけでは当然、スクロール量はとれません。

### scrollをStateに払い出す
scrollは状態ですのでStateに払い出します。recomposeを使いましょう。

```tsx
import * as React from 'react';
import {
    StateHandler,
    StateHandlerMap,
    StateUpdaters,
    withStateHandlers,
} from 'recompose';

const scrollTop = (): number => { // scroll位置を取る関数。
    return Math.max(              // なんかブラウザによってとり方が違うようなので全部もってきてMaxをとる
        window.pageYOffset,
        document.documentElement.scrollTop,
        document.body.scrollTop);
};

type State = { // scrollをStateに払い出し
    scroll: number;
};

interface Updaters extends StateHadlerMap<State> {
    updateScroll: StateHandler<State>; // scrollのupdate関数ファクトリです
}

type Props = State & Updaters;

const component: React.SFC<Props> = (props: Props) => { // componentは当然変更ありません
    return (
        <div>
            Scroll: {props.scroll.toString()}
        </div>
    );
};

const initProps: State = ({ scroll: scrollTop() }); // 初期状態

const stateUpdaters: StateUpdaters<{}, State, Updaters> = {
    updateScroll: (prev: State): StateHandler<State> => (
        (): Partial<State> => ({
            scroll: scrollTop(), // updaterは呼ばれる度にscroll位置をとっては更新する
        })
    ),
};

const enhancedComponent = withStateHandlers<State, Updaters>(
    initProps,
    stateUpdaters
)(component);
```

これで、`enhancedComponent`は`updateScroll`が呼ばれる度に`scroll`が更新されます。

### Event Listenerに登録する
あとはこの`updateScroll`を実際にスクロールされる度に呼ばれるようにしてあげるだけです。
`componentDidMount`で登録してみます。

```tsx
import * as React from 'react';
import {
    compose,
    lifecycle,
    ReactLifeCycleFunctions,
    StateHandler,
    StateHandlerMap,
    StateUpdaters,
    withStateHandlers,
} from 'recompose';

const scrollTop = (): number => {
    return Math.max(
        window.pageYOffset,
        document.documentElement.scrollTop,
        document.body.scrollTop);
};

type State = {
    scroll: number;
};

interface Updaters extends StateHandlerMap<State> {
    updateScroll: StateHandler<State>;
}

type Props = State & Updaters;

const component: React.SFC<Props> = (props: Props) => {
    return (
        <div>
            Scroll: {props.scroll.toString()}
        </div>
    );
};

const initProps: State = ({ scroll: scrollTop() });

const stateUpdaters: StateUpdaters<{}, State, Updaters> = {
    updateScroll: (prev: State): StateHandler<State> => (
        (): Partial<State> => ({
            scroll: scrollTop(),
        })
    ),
};
// ここまでimport文以外は、さっきと同じ

const lifeCycleFunctions: ReactLifeCycleFunctions<Props, {}> = {
    componentDidMount() {
        window.addEventListener('scroll', this.props.updateScroll); // PropsにUpdater渡してあるので呼べる
    },
    componentWillUnmount() {
        window.removeEventListener('scroll', this.props.updateScroll); // Unmount時に外してあげる
    },
};

const enhancedComponent = compose<Props>( // withStateHandlersとlifecycleまとめて適用
    withStateHandlers<State, Updaters>(initProps, stateUpdaters),
    lifecycle<Props, {}>(lifeCycleFunctions),
)(component);
```
これで、実際にスクロールされる度に`updateScroll`が呼び出され、状態がその都度変更されるようになりました。

## スタイルをつける(おまけ)
上に載せたでもは画面の右下に固定するためにちょっとだけスタイルを追加しています。styled-componentsを使って実際に追加してみましょう。diffという形で載せます。

```diff
import * as React from 'react';
+ import styledComponents from 'styled-components';
import {
    compose,
    lifecycle,
    ReactLifeCycleFunctions,
    StateHandler,
    StateHandlerMap,
    StateUpdaters,
    withStateHandlers,
} from 'recompose';

const scrollTop = (): number => {
    return Math.max(
        window.pageYOffset,
        document.documentElement.scrollTop,
        document.body.scrollTop);
};

+ type Outter = {
+     className?: string;
+ };

type State = {
    scroll: number;
};

interface Updaters extends StateHandlerMap<State> {
    updateScroll: StateHandler<State>;
}

type Props
    = State
+     & Outter
    & Updaters;

const component: React.SFC<Props> = (props: Props) => {
    return (
-         <div>
+         <div className={props.className}>
            Scroll: {props.scroll.toString()}
        </div>
    );
};

const initProps: State = ({ scroll: scrollTop() });

- const stateUpdaters: StateUpdaters<{}, State, Updaters>
+ const stateUpdaters: StateUpdaters<Outter, State, Updaters>
    = {
        updateScroll: (prev: State): StateHandler<State> => (
            (): Partial<State> => ({
                scroll: scrollTop(),
            })
        ),
    };

const lifeCycleFunctions: ReactLifeCycleFunctions<Props, {}> = {
    componentDidMount() {
        window.addEventListener('scroll', this.props.updateScroll);
    },
    componentWillUnmount() {
        window.removeEventListener('scroll', this.props.updateScroll);
    },
};

const enhancedComponent
-     = compose<Props>(
+     = compose<Props, Outter>(
        withStateHandlers<State, Updaters>(initProps, stateUpdaters),
        lifecycle<Props, {}>(lifeCycleFunctions),
    )(component);

+ const styled = styledComponents(enhancedComponent)`
+     position: fixed;
+     bottom: 10px;
+     right: 10px;
+     font-weight: bold;
+ `;
```

これで右下に太い字で表示されるようになりますね。

## まとめ
今回出来上がったソース群は以下においてます。
https://github.com/IgnorantCoder/monitor-scroll

今回の趣旨とは違いますが、recomposeを使うと既存のコードに手を入れずにいろいろ追加できてめちゃ便利ですね。