TypeScript
React

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

はじめに

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

何を作るか

縦にスクロールしたときに、縦位置がなんなのかを表示するコンポーネントを書いてみます。
できあがるのは↓こんなやつです。
demo.gif

作ってみる

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

まずはコンポーネントを作ってみる

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

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を使いましょう。

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);

これで、enhancedComponentupdateScrollが呼ばれる度にscrollが更新されます。

Event Listenerに登録する

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

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という形で載せます。

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を使うと既存のコードに手を入れずにいろいろ追加できてめちゃ便利ですね。