カテゴリー
SugiBlog Webエンジニアのためのお役立ちTips

React Contextを使って下位コンポーネントに情報を渡す

通常、親コンポーネントから子コンポーネントにはpropsを使って情報を渡します。
しかし、深くネストされた下位コンポーネントに渡すには、メンテナンスのことも考えると非常に不便です。

コンテクスト(Context)を利用することで情報の受け渡しが簡単になります。

まずは基本の使い方から。
createContextでコンテクストオブジェクトを作成します。
createContextの引数はデフォルト値です。

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

コンポーネントのトップレベルでuseContextを呼び出して、コンテクストを読み取ります。

function Button({ children }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    <button className={className}>
      {children}
    </button>
  );
}

コンテクストを上記のButtonに渡すには、該当のボタンあるいはその親コンポーネントのいずれかを、対応するコンテクストプロバイダでラップします。

export default function MyApp() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  )
}

function Form() {
  // ... renders buttons inside ...
}

これでプロバイダとButtonの間にどれだけ多くのコンポーネントが挟まっていても関係なく情報を受け渡すことができます。

コンテクスト経由で渡されたデータの更新

コンテクストの情報を更新するにはuseStateフックを使って更新します。

function MyPage() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Form />
      <Button onClick={() => {
        setTheme('light');
      }}>
        Switch to light theme
      </Button>
    </ThemeContext.Provider>
  );
}

コンテクスト用のコンポーネントを作成

コンテクストをより汎用的に使うためにコンテクスト用のコンポーネントを作成します。

CountContext.jsx
import { createContext, useState, useContext } from 'react';

const CountContext = createContext();

export function useCountContext() {
    return useContext(CountContext);
}

export function CountProvider({ children }) {
    const [count, setCount] = useState(100);

    const value = {
        count,
        setCount,
    };

    return (
        <CountContext.Provider value={value}>{children}</CountContext.Provider>
    );
}

CountContextコンポーネントをインポートして使ってみましょう。

import { CountProvider, useCountContext } from './CountContext';

function App() {
    return (
        <React.Fragment>
            <CountProvider>
                <Form />
            </CountProvider>
        </React.Fragment>
    )
}

function Form() {
  // ... renders buttons inside ...
}

function MyButton() {
    const { count, setCount } = useCountContext();
    return (
        <Button onClick={() => {
            setCount(count + 1);
        }}>
            add count
        </Button>
    );
}

これで他のコンポートネントから使いやすくなると思います。

コンテクストを介してオブジェクトを更新する

state変数と、それを更新するset関数も含めて下位コンポーネントに渡します。

const CurrentUserContext = createContext(null);

export default function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <CurrentUserContext.Provider
      value={{
        currentUser,
        setCurrentUser
      }}
    >
      <Form />
    </CurrentUserContext.Provider>
  );
}

function Form({ children }) {
  return (
    <Panel title="Welcome">
      <LoginButton />
    </Panel>
  );
}

function LoginButton() {
  const {
    currentUser,
    setCurrentUser
  } = useContext(CurrentUserContext);

  if (currentUser !== null) {
    return <p>You logged in as {currentUser.name}.</p>;
  }

  return (
    <Button onClick={() => {
      setCurrentUser({ name: 'Advika' })
    }}>Log in as Advika</Button>
  );
}

function Panel({ title, children }) {
  return (
    <section className="panel">
      <h1>{title}</h1>
      {children}
    </section>
  )
}

function Button({ children, onClick }) {
  return (
    <button className="button" onClick={onClick}>
      {children}
    </button>
  );
}

オブジェクトや関数を渡すときの再レンダーの最適化

先ほどの方法だとMyAppが再レンダーされるたびに、useContext(CurrentUserContext)を呼び出しているすべてのコンポーネントも再レンダーされてしまいます。
小規模なアプリでは問題になりませんが、currentUserのような内部のデータが変更されていないなら、再レンダーする必要はありません。
データが変わっていないという事実をReactが最大限に活用できるように、set関数をuseCallbackでラップし、オブジェクトの生成をuseMemoでラップすることでこの問題を解決できます。

function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);

  const login = useCallback((response) => {
    setCurrentUser(response);
  }, []);

  const contextValue = useMemo(() => ({
    currentUser,
    login
  }), [currentUser, login]);

  return (
    <CurrentUserContext.Provider value={contextValue}>
      <Page />
    </CurrentUserContext.Provider>
  );
}

下位コンポーネントで利用するには以下のようにします。

function Page() {
    const us = React.useContext(AuthContext);
    // ...
    // us.login({name: '...'});
    // us.currentUser.name,
    // ...
}

もしくは分割代入で使いやすくします。

function Page() {
    const {currentUser, login} = React.useContext(AuthContext);
    // ...
    // login({name: '...'});
    // currentUser.name,
    // ...
}

参考URL:
React::Reference createContext
React::Reference useContext
React::Reference useCallback
React::Reference useMemo

この記事がお役に立ちましたらシェアお願いします
146 views

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です