Reactの基礎 その7: カスタム hooks

新しい現場で本格的にReactを触ることになったのでReactについて学んだことを備忘録として記事にしていきます。
いくつかの記事にわけてReactでステートメントを保持・管理するための機能であるReact hooksについてまとめていきます。

サンプルコードについて

サンプルコードはこちらにあります。
記事ごとにコミットログで分けてあるので該当するコミットをご覧ください。

React hooksとは

React hooksとは関数コンポーネント内の状態やライフサイクルなどのReactの機能を「フック」するために用意されている機能群の総称です。
React hooksを副作用や状態管理の実行などを行うことができ、その用途に合わせて様々な種類のhooksを利用することができます。
React hooksはuse〇〇と名付けられており、〇〇の部分に利用する機能を表す単語が入ります。
前回まではReactで用意されているhooksを紹介してきましたが、今回はそれらのhooksを組み合わせて自分で新たなhooksを作成するカスタムhooksを紹介していきたいと思います。

カスタムhooksとは

React hooksを組み合わせて独自のロジックなどを実装することで特定の役割に特化したhooksを作成することができます。
この独自に作成されたhooksをカスタムhooksと言います。
カスタムhooksを作成することでコンポーネントのロジックを柔軟にすることができ、再利用性を高めることができます。
カスタムhooksを作成する際は、他の用意されているhooksと同様にuse〜という名前をつける必要があります。
use〜とすることでReactによりhooksと認識されるようになるためです。
以下にカスタムhooksの簡単な例を示します。

import { useState } from 'react';

const useCustomHooks = () => {
  const [state, setState] = useState('');

  const setTrimState = (value) => {
    setState(value.trim());
  }

  return [state, setTrimState];
}

こちらの例では、useState()による状態管理を行う際に、文字列である値の前後のスペースを取り除いてからステートにセットするような独自のロジックを実装しています。
このように、再利用するようなロジックを適切にカスタムhooksとして実装することで柔軟性が高まり、コンポーネント内に記述するロジックが減ることでコードの可読性や保守性も高めることができます。
例の中ではuseState()のみを利用していますが、他のhooksを利用して実装することももちろんできます。

カスタムhooks実装例

では、カスタムhooksの実装例を見てみましょう。
今回は、今まで作成してきたTodoアプリのカードを扱うカスタムhooksを作成していきます。
src/hooks/useCards.jsというファイルを作成し、以下のようなカスタムhooksを作成します。

import React, { useReducer, useEffect, createContext, useContext } from "react";
import { fetchCards, toggleStatus } from "../util/cards";
import { getRandomId } from "../util/utils";

const CardsContext = createContext({
  cards: [],
  dispatch: () => {},
});

const reducer = (prevState, action) => {
  switch (action.type) {
    case 'setCards':
      return { cards: action.newCards };
    case 'addCard':
      return { cards: [
        ...prevState.cards,
        {
          id: getRandomId(),
          content: action.value,
          status: 'Not Ready',
        }
      ]};
    case 'toggleCardStatus':
      return {
        cards: prevState.cards.map(c => {
          if (c.id === action.cardId) {
            return {
              ...c,
              status: toggleStatus(c.status),
            };
          } else {
            return c;
          }
        })
      };
    case 'editCard':
      return {
        cards: prevState.cards.map(c => {
          if (c.id === action.cardId) {
            return {
              ...c,
              content: action.newContent,
            };
          } else {
            return c;
          }
        })
      }
    case 'deleteCard':
      return {
        cards: prevState.cards.filter(c => c.id !== action.cardId)
      }
    default:
      throw new Error('無効なアクションです。')
  }
}

export const useCards = () => {
  const [state, dispatch] = useReducer(reducer, { cards: [] });

  useEffect(() => {
    dispatch({ type: 'setCards', newCards: fetchCards() });
  }, []);

  const CardsProvider = ({ children }) => {
    return (
      <CardsContext.Provider
       value={{
        cards: state.cards,
        dispatch,
       }}
      >
        {children}
      </CardsContext.Provider>
    )
  }

  return {
    CardsProvider,
    ...useContext(CardsContext),
  }
};

少し難解になっていますが、このuseCards()の中ではuseReducer()によるcardsの状態管理と更新関数の定義、useContext()によるプロバイダーを作成するためのコンポーネントの定義、useEffect()によるcardsの初期値の取得と設定が行われ、戻り値としてCardsProviderコンポーネントとコンテキスト化されたcardsdispatch関数が連想配列の形で返却されます。
では、useCards()を利用する側も見てみましょう。
まずはApp.js内でuseCards()のプロバイダーを利用してコンポーネント内でコンテキストを読み込めるようにします。

import './App.css';
import { useCards } from './hooks/useCards';
import Header from './Header';
import Body from './Body';

function App() {
  const {
    CardsProvider,
  } = useCards();

  return (
    <CardsProvider>
      <Header />
      <Body />
    </CardsProvider>
  );
}

export default App;

次に、子コンポーネントでコンテキストであるcardsdispatchを利用していきます。
Header.jsから見ていきます。

import React, { useState } from "react";
import { useCards } from "./hooks/useCards";

function Header() {
  const { dispatch } = useCards();
  const [value, setValue] = useState('');

  return (
    <div className='header'>
      <h1 className='title'>Todo List</h1>
      <div className="addCardForm">
        <input
          className="addCardInput"
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
        <button
          className="addCardButton"
          onClick={() => dispatch({ type: 'addCard', value })}
        >追加</button>
      </div>
    </div>
  )
}

export default Header;

useCards()からdispatchを取得して利用しています。
Body.jsCard.jsも同様にuseCards()を利用しています。

import React from "react";
import { useCards } from "./hooks/useCards";
import Card from "./Card";

function Body() {

  const { cards } = useCards();

  return (
    <div className='body'>
      {cards.map(c => {
        return (
          <Card
            key={c.id}
            card={c}
          />
        )
      })}
    </div>
  )
}

export default Body
import React, { useState } from "react";
import { useCards } from "./hooks/useCards";

function Card({ card }) {
  const { dispatch } = useCards();
  const [isEdit, setIsEdit] = useState(false);
  const [value, setValue] = useState(card.content);

  const handleEditClick = () => {
    if (isEdit) {
      dispatch({ type: 'editCard', cardId: card.id, newContent: value });
    }
    setIsEdit(!isEdit);
  }

  return (
    <div className='card'>
      <div className="todoIcon">✔︎</div>
      {isEdit ? (
        <input
          className="todocontent"
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
      ) : (
        <div className='todocontent'>{card.content}</div>
      )}
      <button
        className='editButton'
        onClick={handleEditClick}
      >
        {isEdit ? 'save' : 'edit'}
      </button>
      <div className="todoStatus">
        <span>status:</span>
        <button
          className="statusButton"
          onClick={() => dispatch({ type: 'toggleCardStatus', cardId: card.id })}
        >
          {card.status}
        </button>
      </div>
      <button
        className="deleteButton"
        onClick={() => dispatch({ type: 'deleteCard', cardId: card.id })}
      >
        ✖︎
      </button>
    </div>
  )
}

export default Card;

例では様々なhooksを利用したカスタムhooksを作成しているため少し複雑に見えますが、利用箇所を見ると何となくでもイメージが掴めるのではないでしょうか。
このようにすることでコンポーネント内にcardsに関するロジックがなくなり、useCards()内にまとめることができスッキリさせることができます。

まとめ

今回はカスタムhooksについてまとめてみました。
使い所や作成自体が難しい部分はありますが、Reactの強力な機能であり適切に利用することで多くの利益を得ることができます。
私自身も業務の方では最近やっと自作のカスタムhooksを作成するようになったのですが、難しいながらも楽しい作業だと感じています。
カスタムhooksについてここまで説明してきましたが、個人的に一番勉強になるのは色々な人が作成した様々なカスタムhooksの実装例を見ることだと感じていますので、色々なソースコードに触れると良いと思います。
Reactの基礎シリーズはここまでで一旦終了としたいと思います。
ここまで読んでいただきありがとうございました。
それでは、また。