React Component間の状態管理をContextで解決する方法を理解する #9

JS

Reactでの状態管理をする方法には、useStateを用いたり、useReducerを用いる方法があります。

複数のComponents間に渡るような状態管理をする場合、Component間でpropsを渡していけば、状態管理はできるものの、props chainが長くなるほどコードをメンテするのが大変になります。

そこで本記事では、なぜContextが必要になるかを図を用いて解説した後、Contextを使った方法を見ていきます。

1.ReactにてContextがなぜ必要か?

以下のようなComponent TreeとComponent間で依存関係がある例を見てみます。

上の図のAppのようなComponent Treeにて、useContextを利用しないで、State(状態)を管理したいとしましょう。

Loginイベントにて認証を行い、Add to CartイベントではCart ComponentにProductItemを追加しています。

CartとProductItemに直接的なComponent間のつながりがありません。またLoginFormとShopにも直接的なComponent間のつながりがありません。

認証をしているかどうか判定する場合、propsをComponentに渡していき、App Component経由でShopとCart Componentに渡すことになります。

より大きなアプリで沢山のComponentを使用している場合、props chainが長くなり、また必ずしもpropsが必要でないComponentにも渡すことになります。

これを解決する方法としてContextを利用します。Component間に渡って、Stateを管理できるため、propsを毎回渡していく必要がなくなります。

Component-tree-and-Context

Loginイベントにて、propsをContextに渡します。Add to Cartイベントも同様にContext経由になります。

2.React Context API

以前の記事でLogin.jsにてuseReducerを利用する記事を紹介しました。

Login.jsの中身を前回の記事で確認頂くことをオススメします。

本記事では、以下のようなComponent Treeを例にContextを利用していきます。

component-tree-using-context

新しいフォルダstoreを作成します。

フォルダ内にauth-context.js(ケバブケースで)ファイルを作成します。

(命名に制限は特にないが、AuthContext.jsのようなパスカルケースだとComponentがあることを暗示するため、ケバブケースを使用)

src/store/auth-context.js

import React from "react";

const AuthContext = React.createContext({
  isLoggedIn: false
});

export default AuthContext;

createContextには初期のContextを設定します。Contextはstringでもいいし、objectでもOKです。ここではobjectを設定します。

AuthContextはComponentを含んだobjectになります。

このAuthContextを利用するには、Provideする必要があります。

Provideとは、JSXコード内で全てのComponentをラップして、ComponentがContextにアクセスできるようにすることを意味します。

ここでは、App.jsにてAuthContextでラップします。

src/App.js

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

import Login from './components/Login/Login';
import Home from './components/Home/Home';
import MainHeader from './components/MainHeader/MainHeader';
import AuthContext from './store/auth-context';

function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(()=> {
    const storageLoggedIn = localStorage.getItem('isLoggedIn');

    if(storageLoggedIn === '1') {
      setIsLoggedIn(true);
    }
  }, []);

  const loginHandler = (email, password) => {
    localStorage.setItem('isLoggedIn', '1');
    setIsLoggedIn(true);
  };

  const logoutHandler = () => {
    localStorage.removeItem('isLoggedIn');
    setIsLoggedIn(false);
  };

  return (
    <AuthContext.Provider>
      <MainHeader isAuthenticated={isLoggedIn} onLogout={logoutHandler} />
      <main>
        {!isLoggedIn && <Login onLogin={loginHandler} />}
        {isLoggedIn && <Home onLogout={logoutHandler} />}
      </main>
    </AuthContext.Provider>
  );
}

export default App;

JSXコードでは、Componentでラップする必要があるため、

<AuthContext.Provider></AuthContext.Provider>

のようにラップします。

これにて、App Component配下の全ての子Componentが、AuthContextにアクセスできるようになりました。

value(object)にアクセスできるようにするため、以下のようにvalueを追加して、objectを指定します。

src/App.js

 return (
   <AuthContext.Provider value={{
     isLoggedIn: isLoggedIn,
   }}>
     <MainHeader onLogout={logoutHandler} />
     <main>
       {!isLoggedIn && <Login onLogin={loginHandler} />}
       {isLoggedIn && <Home onLogout={logoutHandler} />}
     </main>
   </AuthContext.Provider>
  );
}

またMainHeaderの方は以下のようにしています。

src/components/MainHeader/MainHeader.js

import React from 'react';

import Navigation from './Navigation';
import classes from './MainHeader.module.css';

const MainHeader = (props) => {
  return (
    <header className={classes['main-header']}>
      <h1>A Context Sample Page</h1>
      <Navigation onLogout={props.onLogout} />
    </header>
  );
};

export default MainHeader;

以前はJSXコード内のNavigationにて、以下のようにisLoggedInを渡していましたが、上記のように渡す必要がなくなりました。

<Navigation isLoggedIn={props.isAuthenticated} onLogout={props.onLogout} />

3.useContext Hook

<AuthContext.Provider>で子Componentをラップした後、子供側にて、それを受け取る(listen)する必要があります。

1つのやり方としては、Consumerを使う方法があります。

<AuthContext.Consumer>
  { (ctx) => { … } }
</AuthContext.Consumer>

以下のようにNavigation.js内でctxからisLoggedInを受け取り、ログインしているかどうかでheaderメニューの表示を制御しています。

src/components/MainHeader/Navigation.js

import React from 'react';

import classes from './Navigation.module.css';
import AuthContext from '../../store/auth-context';

const Navigation = (props) => {
  return (
    <AuthContext.Consumer>
      {(ctx) => {
        return (
          <nav className={classes.nav}>
            <ul>
              {ctx.isLoggedIn && (
                <li><a href="/">Users</a></li>
              )}
              {ctx.isLoggedIn && (
                <li><a href="/">Admin</a></li>
              )}
              {ctx.isLoggedIn && (
                <li>
                  <button onClick={props.onLogout}>Logout</button>
                </li>
              )}
            </ul>
          </nav>
        )
      }}
    </AuthContext.Consumer>
  );
};

export default Navigation;

もう1つは、useContextを利用する方法になります。

src/components/MainHeader/Navigation.js

import React, { useContext } from 'react';

import classes from './Navigation.module.css';
import AuthContext from '../../store/auth-context';

const Navigation = (props) => {
  const ctx = useContext(AuthContext);

  return (
    <nav className={classes.nav}>
      <ul>
        {ctx.isLoggedIn && (
          <li><a href="/">Users</a></li>
        )}
        {ctx.isLoggedIn && (
          <li><a href="/">Admin</a></li>
        )}
        {ctx.isLoggedIn && (
          <li>
            <button onClick={props.onLogout}>Logout</button>
          </li>
        )}
      </ul>
    </nav>
  );
};

useContextをJSXコード内で呼び出した後、AuthContextをポインターとして指定します。

Consumerのときと同様に、ctxからisLoggedInを取得できるので、headerメニューの表示制御ができています。

ここまでのデモとして動作確認をすると以下のような感じになります。

login-demo-with-context

動作自体は、別の記事でも上記のようなログインフォームにメールアドレスとパスワードに値を入力したら、Loginボタンが押せるようになり、押下後、headerメニューにLogoutボタンが表示されるというデモアプリです。

(LocalStorageを利用しているので、Loginボタン押下後、isLoggedInとvalueがStorageにできていることがわかります。)

以下のようにvalueにLogoutHandlerを渡すこともできます。

src/App.js

 return (
   <AuthContext.Provider value={{
     isLoggedIn: isLoggedIn,
     onLogout: logoutHandler
   }}>
     <MainHeader onLogout={logoutHandler} />
      …
   </AuthContext.Provider>
);

Navigation.jsでは、propsからonLogoutを受け取っていましたが、これをcontextから受け取れます。

src/components/MainHeader/Navigation.js

{ctx.isLoggedIn && (
  <li>
    <button onClick={ctx.onLogout}>Logout</button>
  </li>
)}

src/store/auth-context.js

const AuthContext = React.createContext({
  isLoggedIn: false,
  onLogout: () => {},
});

onLogoutを追加していなくても動作はしますが、追加しておくと、VSCodeなどのIDEでctxから呼び出すときにサジェストされるようになります。

4.カスタムContext Provider

上記で紹介したしたauth-contex.jsでは、初期値を持たせているだけでした。

auth-context.jsでも処理を持たせることができます。

今までApp.js内で書いていた処理をauth-context.js内で以下のように書くことができます。

src/store/auth-context.js

import React, { useState, useEffect } from "react";

const AuthContext = React.createContext({
  isLoggedIn: false,
  onLogout: () => {},
  onLogin: (email, password) => {}
});

export const AuthContextProvider = (props) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  useEffect(()=> {
    const storageLoggedIn = localStorage.getItem('isLoggedIn');

    if(storageLoggedIn === '1') {
      setIsLoggedIn(true);
    }
  }, []);

  const logoutHandler = () => {
    localStorage.removeItem('isLoggedIn')
    setIsLoggedIn(false);
  }

  const loginHandler = () => {
    localStorage.setItem('isLoggedIn', '1');
    setIsLoggedIn(true);
  }

  return (
    <AuthContext.Provider value={{
      isLoggedIn: isLoggedIn,
      onLogout: logoutHandler,
      onLogin: loginHandler,
    }}>
      {props.children}
    </AuthContext.Provider>
  )
}

export default AuthContext;

App.js内の処理をauth-contex.jsにまとめることができたので、App.js内で記載していた処理は必要ないため、以下のように書き直せます。

src/App.js

import React from 'react';

import Login from './components/Login/Login';
import Home from './components/Home/Home';
import MainHeader from './components/MainHeader/MainHeader';
import AuthContext from './store/auth-context';
import { useContext } from 'react';

function App() {
  const ctx = useContext(AuthContext);

  return (
    <React.Fragment>
      <MainHeader onLogout={ctx.logoutHandler} />
      <main>
        {!ctx.isLoggedIn && <Login />}
        {ctx.isLoggedIn && <Home />}
      </main>
    </React.Fragment>
  );
}

export default App;

LoginとHomeでonLoginとonLogoutを持たせる必要がなくなるため、消しています。

src/index.js

import React from 'react';

import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { AuthContextProvider } from './store/auth-context';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<AuthContextProvider><App /></AuthContextProvider>);

index.jsにて、App ComponentをAuthContextProviderで囲みます(ラップします)。

src/components/Home/Home.js

import React from 'react';
import { useContext } from 'react';
import AuthContext from '../../store/auth-context';

import Card from '../UI/Card/Card';
import classes from './Home.module.css';

const Home = (props) => {
  const authCtx = useContext(AuthContext);

  return (
    <Card className={classes.home}>
      <h1>Welcome back!</h1>
      <button onClick={authCtx.onLogout}>Logout</button>
    </Card>
  );
};

export default Home;

Home.js内では今まで、propsからonLogoutを取得していましたが、useContextとAuthContextを使って、authCtx.onLogoutという形に変更しています。

最後にLogin.jsもHome.js同様に変更します。

(一部記載、全体のLogin.jsは前回記事参照)

src/components/Login/Login.js

import React, { useState, useEffect, useReducer, useContext } from 'react';

import Card from '../UI/Card/Card';
import classes from './Login.module.css';
import Button from '../UI/Button/Button';
import AuthContext from '../../store/auth-context';
…
const Login = (props) => {
…
  const authCtx = useContext(AuthContext);
…
  const submitHandler = (event) => {
    event.preventDefault();
    authCtx.onLogin(emailState.value, passwordState.value);
    // props.onLogin(emailState.value, passwordState.value);
  };
…

5.まとめ

  • 複数のComponents間に渡る変数等の状態管理には、useState, useReducerよりContextを活用した方がよい場合がある。
  • Context ファイルを作成して、AuthContext.Provider (Component)で App(ルートレベル)Componentをラップする(囲む)ことで、その配下の全てのComponentがContext経由で変数等にアクセス可能になる。
  • AuthContext.Providerrで子Componentをラップした後、子供側で受け取れるようにするには、AuthContext.Consumerを利用するか、useContextを利用する方法がある。
  • useContextを、子Componentでimport宣言した後、useContext(AuthContext)のように書くことで、子ComponentからContextにアクセスする/または変数等を受け取ることができる。