Reactの基礎 その5:React hooks その3 useContext()

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

サンプルコードについて

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

React hooksとは

React hooksとは関数コンポーネント内の状態やライフサイクルなどのReactの機能を「フック」するために用意されている機能群の総称です。
React hooksを副作用や状態管理の実行などを行うことができ、その用途に合わせて様々な種類のhooksを利用することができます。
React hooksはuse〇〇と名付けられており、〇〇の部分に利用する機能を表す単語が入ります。
React hooksはあらかじめ用意されている機能が多数あり、いくつかの記事に分けて紹介していきたいと思います。
今回は複数の階層のコンポーネントに跨った状態(コンテキスト)を管理することができるuseContext()についてまとめていきます。

useContext()とは

useContext()は状態管理を行うReact hooksで、アプリのコンテキストを管理することができます。
コンテキストとは平たくいうとコンポーネントに跨る状態のことで、useContext()を利用することによって深い階層にあるコンポーネントがpropsの受け渡しをせず直接アクセスすることができるようになります。
そのため、ネストされたコンポーネント全体で利用する値などを管理するのに利用されます。
基本的な使い方は以下の通りです。

まず初めに、そのコンテキストを利用する最上位層のコンポーネントでコンテキストを作成します。
createContext()を使ってコンテキストを作成します。
その後、Context.Providerコンポーネントを利用してコンテキストを利用するコンポーネントを定義します。
その際、valueのpropsにコンテキストとして利用する値をキーバリューで指定します。

import React, { createContext } from 'react';
import { ChildComponent } from './ChildComponent';

// Contextの作成
export const MyContext = createContext();

export const ParentConponent = () => {
  return (
    <MyContext.Provider
       value={ key: value /* コンテキストとして持つ値を定義 */ }
    >
      <ChildComponent />
    </MyContext.Provider>
  );
};

これでコンテキストを利用するための準備ができました。
では、子コンポーネントでコンテキストを利用してみましょう。
利用する際はuseContext()の引数に利用するコンテキストを渡します。

import React, { useContext } from 'react';
import { MyContext } from './ParentComponent';

export const ChildComponent = () => {
  // Contextの呼び出し
  const context = useContext(MyContext);

  return (
    <div>
      {context.value /* コンテキストの値を利用 */} 
    </div>
  )
};

propsの受け渡しをすることなく値を利用できています。
useContext()で受け渡せるものは値だけでなく関数も渡すことができます。
サンプルコードの実装例で見てみましょう。

useContext()実装例

今回は以下のような画面を作成していきます。
Todo Listのカードの状態をアプリ内で管理しているのですが、カードの表示とステータスの切り替えはカードコンポーネントから行い、カードの追加はヘッダーコンポーネントから行います。

useContext()を利用することでそれぞれのコンポーネントから直接Todoカードの状態を管理することができるようになります。
まずはApp.jsでコンテキストとプロバイダーを作成しましょう。
ここではuseState()を利用したカードの状態管理のための値と関数が全て定義され、プロバイダーにそれらを渡しています。

import './App.css';
import { useState, useEffect, createContext } from 'react';
import Header from './Header';
import Body from './Body';
import { getRandomId } from './util/utils';
import { fetchCards } from './util/cards';
export const CardsContext = createContext();

function App() {
  const [cards, setCards] = useState([]);

  const toggleStatus = (status) => {
    switch(status) {
      case 'Not Ready':
        return 'Ready';
      case 'Ready':
        return 'Compleated';
      case 'Compleated':
        return 'Not Ready';
      default:
        throw new Error('無効なステータスです');
    }
  }

  const toggleCardStatus = (cardId) => {
    const newCards = cards.map(c => {
      if (c.id === cardId) {
        return {
          ...c,
          status: toggleStatus(c.status),
        };
      } else {
        return c;
      }
    });
    setCards(newCards);
  }

  const addCard = (content) => {
    const newCards = [
      ...cards,
      {
        id: getRandomId(),
        content,
        status: 'Not Ready',
      }
    ]
    setCards(newCards);
  }

  useEffect(() => {
    setCards(fetchCards());
  }, []);

  return (
    <CardsContext.Provider
      value={{
      cards,
      setCards,
      toggleCardStatus,
      addCard,
      }}
    >
      <Header />
      <Body />
    </CardsContext.Provider>
  );
}

export default App;

では、各コンポーネントでコンテキストを利用していきます。
Header.jsではカードを追加するため、コンテキストからaddCard()関数を取り出しボタンクリックで実行させています。

import React, { useState, useContext } from "react";
import { CardsContext } from "./App";

function Header() {
  const { addCard } = useContext(CardsContext);
  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={() => addCard(value)}
        >追加</button>
      </div>
    </div>
  )
}

export default Header;

Body.jsではCardコンポーネントを作成するためにcardsの値を受け取りその要素を元にコンポーネントの作成を行っています。

import React, { useContext } from "react";
import Card from "./Card";
import { CardsContext } from "./App";

function Body() {

  const { cards } = useContext(CardsContext);

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

export default Body

Card.jsではカードのステータスを切り替えるのでtoggleCardStatus()関数をコンテキストから受け取り利用しています。

import React, { useContext } from "react";
import { CardsContext } from "./App";

function Card({ card }) {
  const { toggleCardStatus } = useContext(CardsContext);

  return (
    <div className='card'>
      <div className="todoIcon">✔︎</div>
      <div className='todocontent'>{card.content}</div>
      <div className="todoStatus">
        <span>status:</span>
        <button
          className="statusButton"
          onClick={() => toggleCardStatus(card.id)}
        >
          {card.status}
        </button>
      </div>
    </div>
  )
}

export default Card;

この例から分かるように、useContext()を利用することでApp.js内にcardsの値の状態管理を行う関数を全てまとめ、各コンポーネントでは必要なものだけを受け取り利用することができます。
このようにすることで、コンポーネント間の受け渡しなどの複雑性を無くすことができ、可読性の高いコードにすることができます。

まとめ

今回はuseContext()についてまとめてきました。
複数のコンポーネントで共通して利用する値を管理する場合はuseState()とpropsを利用するよりもuseContext()を利用する方が良いでしょう。
かなり利用頻度の高いhooksなのでその特徴をしっかりと認識して正しく利用できるようにしましょう。
ここまで読んでいただきありがとうございました。
それでは、また。