useContextの使い方

業務で一時的に新プロジェクトにアサインされた際にuseContextを多用していた。
Reduxはよっぽどの時じゃないと使わない方針ぽい。

確かにReduxはファイル数多くなるし何かとややこしいことが多い。
Reduxを使うかどうかは以下の記事がわかりやすかった。

https://ics.media/entry/200409/

useContextの使い方はだいぶ理解できてきたのでここでまとめる。
useReducerは正直まだよくわかってないから別の機会に。

目次

useContextはどういう時に使う?

コンポーネントをまたいで値を渡す時。

propsで渡すと孫コンポーネントに渡す時に、一度子コンポーネントを経由しなければならなくなりバケツリレーになってしまう。(propドリルとか言われてるっぽい)

さらにContextのいい点はContextで管理している状態や関数をそれぞれの子コンポーネントで共通して扱えるということ。

ここがReduxっぽいけどReduxみたいなstoreとは違って、特定の範囲内にしか渡せないからスコープが限られている。

まぁ実際、それで困ったことはないけど。
というかそれで困るならコンポーネントの設計見直した方がいいのではとさえ思っている。

それはさておきContextの概念を図にするとこんな感じになる
以下の図は、AuthContextという認証関連の変数や操作ロジックを扱ったContextと、
それを実際に使用する2つのコンポーネントとの関係性を図示したもの。

・ログインしているかどうかを示す変数であるisAuthがAuthContextで一元的に管理されている

・この子コンポーネントはisAuthを使えるようになり、isAuthの値に応じた処理を書くことができるようになる

・コンポーネントAではloginHandlerを呼び出すことで、AuthContext内のisAuthをtrueに変更することができる

useContextを使うと、こんな感じでコンポーネントで共通のものを管理することができるのが最大の特徴である。

ただ、今のプロジェクトでは1つのコンポーネントでしか使わない変数やロジックもContextに分けている。
意図としては各コンポーネントはレンダリングに関する描画だけを担当し、stateの更新やボタンクリック時の操作などのロジックは全部切り分けたいからというもの。

ぶっちゃけこの辺はもう好みだと思う。
個人的にはロジックが複雑じゃないstate変更だけとかなら別ファイルに切り分けるのも面倒だし可読性下がる気もするからそのくらいならいいかなとも思ってしまう。

ただコンポーネントのテストをする時にロジックが綺麗に分断されてるとテストしやすいのかなーとも思ったり。。

ReduxとContextどっちを使ったらいいの?

結論正解はないんだけど、個人的にはContextの方が好きだし、職場のエンジニアも全員そんな感じ。

理由はReduxはファイル数多くなるし記述が長くなってしまってややこしくなるから。

詳細な比較は別記事に譲るが、ざっと調べた感じ大規模なプロジェクトだとReduxの方がいいかもだけど、中小規模ならContextで十分という印象。

あとReduxはサードパーティ製だけど、useContextはhooksなので純正なReactになるというのも嬉しいポイント。

Contextの使い方

実際の使い方を見ていく前に状況整理

今回はこういうファイル構成にしてComponentAとComponentBでそれぞれ共通の変数を扱うようにする。
実際にはありえないがまぁ今回はよしとする。

├── App.jsx
├── AuthContext.jsx
├── ComponentA.jsx
└── ComponentB.jsx

Step 1. Contextの作成

まずは子コンポーネントで使う変数や関数を管理するためのContextを作成する。
ここではAuthContextを作成する。

import React, { useState, createContext } from 'react';

// Contextオブジェクトを作成し、exportする
export const AuthContext = createContext();

const AuthContextProvider = props => {
  const [isAuth, setIsAuth] = useState(false);

  const login = () => {
    setIsAuth(true);
  }

  return (
    // Providerで子コンポーネントをラップする
    // valueに子コンポーネントで使いたい変数や関数を与える
    <AuthContext.Provider value={{login, isAuth}}>
      {props.children}
    </AuthContext.Provider>
  )
}

export default AuthContextProvider;

ポイントは以下の2つ。

①createContextでContextオブジェクトを作成し、外部で使えるようにexportする
②子コンポーネントをContextオブジェクトのProviderでラップすることでvalueに記載した変数や関数を子コンポーネントで使用できるようにする

createContextはContextオブジェクトを作り出していて、これによってProviderとConsumerができるらしい。
Providerは与える側、Consumerは受け取る側。
hooksではConsumerはuseContextで代用される。

createContextに初期値を与えている例も見るけど、結論必要ないっぽい。
初期値はuseContextがProviderを見つけることができないときにのみ参照されるらしいが、Context使う以上ユースケースとしてそれは考えにくいのでちゃんと実装できていれば必要ないのではないかと思っている。

以下の記事がわかりやすかった

https://uncle-javascript.com/react-context-beginning

Step 2. Providerを適切な位置に配置する

import { ComponentA } from "./ComponentA";
import { ComponentB } from "./ComponentB";
import AuthContextProvider from "./AuthContext";

function App() {
  return (
    <BrowserRouter>
      <AuthContextProvider>
        <Switch>
          <Route path="/a" component={ComponentA} />
          <Route path="/b" component={ComponentB} />
        </Switch>
      </AuthContextProvider>
    </BrowserRouter>
  );
}

export default App;

理論的には子コンポーネントをラップできればどこでもいいんだけど、今回はここでラップしている。

そのため、子コンポーネントであるComponentAとComponentBではStep1のAuthContextProviderで定義した変数と関数を使える準備ができたということになる。

Step 3. 子コンポーネントでContextを使う

ComponentA

import React, { useContext } from "react";
import { AuthContext } from "./AuthContext";
import { Link } from "react-router-dom";

const ComponentA = () => {
  const { isAuth, login } = useContext(AuthContext);

  return (
    <div className="auth">
      <h2>ComponentA</h2>
      {isAuth ? (
        <h2>認証されました</h2>
      ) : (
        <>
          <h2>認証されてません</h2>
          <button onClick={login}>ログイン</button>
        </>
      )}
      <Link to="/b">ComponentBへ</Link>
    </div>
  );
};

export default ComponentA;

①useContextをimoprt
②useContextの引数にAuthContextで作成したContextオブジェクト(Step 1で作成)を入れる
③使いたいオブジェクトを取り出す(ここではisAuthとlogin)

あとは取り出したオブジェクトをコンポーネント内で好きなように使えば良い。

ここではisAuthの真偽によって表示する内容を変えている。
さらにonClickイベントでログイン関数を呼び出している。

ComponentB

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

const ComponentB = () => {
  const { isAuth } = useContext(AuthContext);

  return (
    <div>
      <h2>ComponentB</h2>
      {isAuth ? <h2>認証されました</h2> : <h2>認証されてません</h2>}
      <Link to="/a">ComponentAへ</Link>
    </div>
  );
};

export default ComponentB;

ComponentBも同様にisAuthを取り出して表示内容を変えている。

Contextによって何ができるようになったのか?

これまでの実装で以下のようなことができるようになった

①ComponentAでログインボタンを押すことでlogin関数が呼び出される

②AuthContextのisAuthがfalseからtrueに変更される

③ComponentA, ComponentBでそれぞれ変更されたisAuthの値を使えるようになった

つまりComponentAによる値の変更をComponentBで検知できるようになった。

これはpropsでやってることと意味合いは同じだが、兄弟コンポーネントや孫コンポーネント など、AuthContextProviderでラップした子コンポーネント内ならどこからどこへでも変数や関数を共有化できることを意味している。

実際の挙動はこんな感じになる。

きれいにロジックも分離できていい感じ!

おわりに

Contextは存在は知っていて、なんとなーくだけわかってるレベルだったんだけど業務で使ってたら慣れた

どこまでロジックをContextに任せるかは人によって変わりそうだけど、ロジックを分離するために有用なのは間違いない。

ちなみにカスタムフックについても難しそうと思ってたけど使ってみたら意外とそうでもなかったので、別の機会にまとめる。

ちょっとずつできること増やしていきたいな。
あとはuseReducerの使い方がいまだによくわかっていないから今度ちゃんと学ぶ。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次