wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

React Custom Hooksでコンポーネントを整理

コンポーネントのコードにキーボードイベントの処理やスクロールイベントなど、 直接UIに関わらない処理が混じって、コードの可読性がしていたのでReact Custom Hooksを使用して整理しました。

ja.reactjs.org

React Custom Hooksはコンポーネントとロジックのコードを分離するために、よく使われます。 今回は複数の画面で使用してかつ、直接UIに関わらない部分についてCustom Hooksを使用してロジックの分離を行いました。

Pull Request

github.com

実装

ソフトウェアキーボードの表示判定

ペペロミアでは以下の画像のようにソフトウェアキーボードが表示中は右上にキーボードアイコンを表示させて アイコンをタッチするとキーボードを閉じる動作を実装しています。

f:id:wheatandcat:20200810130500p:plain:w480

これを愚直に実装すると、以下の感じになります。

import React, { useState, useEffect } from 'react';
import { View, Text, Keyboard } from 'react-native';

type Props = {}

const Page: React.FC<Props> = (props) => {
  const [showKeyboard, setShow] = useState(false);
  
  useEffect(() => {
    Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
    Keyboard.addListener('keyboardDidHide', _keyboardDidHide);

    return () => {
      Keyboard.removeListener('keyboardDidShow', _keyboardDidShow);
      Keyboard.removeListener('keyboardDidHide', _keyboardDidHide);
    };
  }, []);

  const _keyboardDidShow = () => {
    setShow(true);
  };

  const _keyboardDidHide = () => {
    setShow(false);
  };


  return (
    <View>
      { showKeyboard ? <Text>キーボード開いている</Text> :   <Text>キーボード閉じている</Text>}
    </View>
  )

}

上記の例はキーボードのみの処理のみなので、まだ読めますが、実際の実装は以下みたいに読みづらいコードになっていました。

https://github.com/wheatandcat/Peperomia/blob/9af0142d3625f205ac7d318270ab677be1465377/src/components/templates/CreatePlan/Page.tsx

上記の処理からReact Custom Hooksを使用して分割すると以下みたいになります。

src/hooks/useKeyboard.tsx

import { useState, useEffect } from 'react';
import { Keyboard } from 'react-native';

const useKeyboard = () => {
  const [showKeyboard, setShow] = useState(false);

  useEffect(() => {
    Keyboard.addListener('keyboardDidShow', _keyboardDidShow);
    Keyboard.addListener('keyboardDidHide', _keyboardDidHide);

    // cleanup function
    return () => {
      Keyboard.removeListener('keyboardDidShow', _keyboardDidShow);
      Keyboard.removeListener('keyboardDidHide', _keyboardDidHide);
    };
  }, []);

  const _keyboardDidShow = () => {
    setShow(true);
  };

  const _keyboardDidHide = () => {
    setShow(false);
  };

  return { showKeyboard };
};

export default useKeyboard;

上記を利用してコーディングすると以下の通りになります。

import React from 'react';
import { View, Text } from 'react-native';
import useKeyboard from 'lib/useKeyboard';

type Props = {}

const Page: React.FC<Props> = (props) => {
  const { showKeyboard } = useKeyboard();

  return (
    <View>
      { showKeyboard ? <Text>キーボード開いている</Text> :   <Text>キーボード閉じている</Text>}
    </View>
  )

}

だいぶスッキリしましたね。 ※実際のコードは、こんな感じになりました。

https://github.com/wheatandcat/Peperomia/blob/master/src/components/templates/CreatePlan/Page.tsx

他にも以下をReact Custom Hooksで実装しました。

スクロール位置を取得

src/hooks/useScroll.tsx

import { useState } from 'react';
import {
  NativeSyntheticEvent,
  TextInputScrollEventData,
  Platform,
  StatusBar,
} from 'react-native';
import { getStatusBarHeight } from 'react-native-status-bar-height';

const top =
  Platform.OS === 'android' ? StatusBar.currentHeight : getStatusBarHeight();

const useScroll = (offsetY: number = 84) => {
  const [scrollBelowTarget, setScrollBelowTarget] = useState(true);

  const onScroll = (e: NativeSyntheticEvent<TextInputScrollEventData>) => {
    const offsetScrollY = offsetY + (top || 0);

    if (e.nativeEvent.contentOffset.y >= offsetScrollY && scrollBelowTarget) {
      setScrollBelowTarget(false);
    }

    if (e.nativeEvent.contentOffset.y < offsetScrollY && !scrollBelowTarget) {
      setScrollBelowTarget(true);
    }
  };

  return {
    onScroll,
    scrollBelowTarget,
  };
};

export default useScroll;

スケジュールのタイトル入力からサジェストを管理

src/hooks/useItemSuggest.tsx

import { useCallback, useState } from 'react';
import { useItems } from 'containers/Items';
import { SuggestItem, uniqueSuggests } from 'lib/suggest';

const useItemSuggest = () => {
  const { items, itemDetails } = useItems();
  const [suggestList, setSuggest] = useState<SuggestItem[]>([]);

  const getSuggestList = useCallback((): SuggestItem[] => {
    const suggestList1 = (items || []).map((item) => ({
      title: item.title,
      kind: item.kind,
    }));
    const suggestList2 = (itemDetails || []).map((itemDetail) => ({
      title: itemDetail.title,
      kind: itemDetail.kind,
    }));

    const r = [...suggestList1, ...suggestList2];
    return r;
  }, [items, itemDetails]);

  const setSuggestList = useCallback(
    (title: string) => {
      const r = uniqueSuggests(getSuggestList())
        .filter((item) => {
          if (!title) {
            return false;
          }
          return item.title.includes(title);
        })
        .slice(0, 8);

      setSuggest(r);
    },
    [getSuggestList]
  );

  return {
    setSuggestList,
    suggestList,
  };
};

export default useItemSuggest;

最後に

まだReact Custom Hooksでコードをシンプル出来る箇所がありそうなので、ちょいちょいリファクタリング進めて行く予定