React Context(useContext)入門

以前解説したReact ContextのHook版チュートリアルになります。ステートレスコンポーネントにおいては、こちらを使う方がシンプルになります。

React Contextとは

Reactコンテキストを使用すると、親コンポーネントの状態変数をpropsを経由させずに引き渡すことが可能となります。シンプルかつ標準APIのため好まれています。

詳細を更に知りたい場合は、過去のContext記事を参照してください。

ベースとなるコード

例を作り上げていく形式で解説します。フォームに入力した数値がフォントサイズとして適用される例を考えてみます。

step1

// index.js

import React, { useState, createContext } from 'react';
import ReactDOM from 'react-dom';

function App() {
  const [size, setSize] = useState(16);

  return (
    <div>
        <h1 style={{ fontSize: `${size}px` }}>FONT SIZE {size}px</h1>
        <form onSubmit={e => e.preventDefault()}>
          <input
            type="number"
            value={size}
            onChange={e => setSize(e.target.value)}
          />
        </form>
    </div>
  );
}

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

上記のsizeをコンテキストを使って、子コンポーネント内でも利用できるようにします。

コンテキストを使い子コンポーネントで値を参照する

まずメインコードのindex.jsでコンテキストを作成します。


// index.js

import React, { useState, createContext } from 'react';
import ReactDOM from 'react-dom';
import Text from './Text';

export const FontContext = createContext(0);

function App() {
  const [size, setSize] = useState(16);

  return (
    <div>
      <FontContext.Provider value={size}>
        <Text />
        <form onSubmit={e => e.preventDefault()}>
          <input
            type="number"
            value={size}
            onChange={e => setSize(e.target.value)}
          />
        </form>
      </FontContext.Provider>
    </div>
  );
}

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

createContext()でコンテキストを作成しています。引数はデフォルト値になりますが、実際は初期化するので何を入れても問題ありません。

そして<FontContext.Provider value={size}>でコンテンツをラップします。valueに代入する値がコンテキストで共有される値となります。State Hookのsizeを設定しています。

次にコンテキストの値を利用する子コンポーネントText.jsxを作成します。


// Test.jsx

import React, { useContext } from 'react';
import { FontContext } from './index';

export default function Text() {
  const size = useContext(FontContext);

  return <h1 style={{ fontSize: `${size}px` }}>FONT SIZE {size}px</h1>;
}

従来のコンテキストを使用したことがあるならば、useContext()Consumerに該当します。要はコンテキストを参照するAPIです。ここでは親コンポーネントでセットしたsizeを見出し要素のスタイルとして利用しています。

実行結果は先程と同じですが、子コンポーネントでsizeが利用できるようになりました。

子コンポーネントからも更新ができるようにする

現状は子コンポーネントは値の参照だけを行なっています。子からも値の変更ができるようにしましょう。最終コードとプレビューはSandboxで確認できます。

createContext()に渡す初期値を配列形式にします。第1引数は先程のままですが、第2引数は空の関数を入れておきます。更新用の関数が入るという意味です。

export const FontContext = createContext([0, () => {}]);

そして、Providerにも同じように値と更新関数を配列形式で渡します。

<FontContext.Provider value={[size, setSize]}>

直接関係はないのですが子コンポーネントとの兼ね合い上、更新値がStringとして見なされるため、既存のsetSize()内を明示的に数値変換させておきます。

<input
    type="number"
    value={size}
    onChange={e => setSize(parseInt(e.target.value, 10))}
/>

次にText.jsxコンポーネントです。

こちらは上記で追加された更新関数も一緒にuseContext()で受け取るようにしています。更新関数はこのコンポーネントで定義できるので、ボタンクリックのたび4pxフォントサイズを上げる処理を定義しています。


// Text.jsx

import React, { useContext } from 'react';
import { FontContext } from './index';

export default function Text() {
  const [size, setSize] = useContext(FontContext);

  return (
    <div>
      <h1 style={{ fontSize: `${size}px` }}>FONT SIZE {size}px</h1>
      <button onClick={e => setSize(parseInt(size + 4, 10))}>+4</button>
    </div>
  );
}

step2

子からコンテキストの更新関数を呼び出し、コンテキストの値を変更できました。

複数の値をコンテキストで管理する

コンテキストに複数の値を持たせたい場合もあるでしょう。その場合は、単にcreateContextに渡す値をオブジェクトにするだけです。

ここではフォントのサイズだけでなく、色もコンテキストで管理することにしました。


export const FontContext = createContext([
    { size: 0, color: 'red' }, () => {}
]);

この場合、コンポーネント内で対応しているState Hook値もオブジェクトにします。

const [value, setValue] = useState({
  size: 16,
  color: '#FF0000'
});

Stateと更新関数の名前を変えたので、Providerのvalue内変数名も変更します。

<FontContext.Provider value={[value, setValue]}>

フォントサイズ変更時のイベントハンドラは対応するState値がオブジェクトに変わったことで、別に切り分けました。クラスベースのsetStateと異なりHookの更新関数はState値を丸ごと置き換えるため、工夫が必要です。

const updateHandler = e => {
  setValue({
    ...value,
    [e.target.name]: e.target.value
  });
};

index.jsの全文は以下になります。


import React, { useState, createContext } from 'react';
import ReactDOM from 'react-dom';
import Text from './Text';

export const FontContext = createContext([
    { size: 0, color: 'red' }, () => {}
]);

function App() {
  const [value, setValue] = useState({
    size: 16,
    color: '#FF0000'
  });

  const updateHandler = e => {
    setValue({
      ...value,
      [e.target.name]: e.target.value
    });
  };

  return (
    <div>
      <FontContext.Provider value={[value, setValue]}>
        <Text />
        <form onSubmit={e => e.preventDefault()}>
          <input
            type="number"
            name="size"
            value={value.size}
            onChange={updateHandler}
          />
        </form>
      </FontContext.Provider>
    </div>
  );
}

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

残りはText.jsです。1ステップ前のボタンの加算処理は削除し、新たにコンテキストで取り扱っているフォントの色(color)の変更を行うイベントハンドラを追加しています。

以下はText.jsxの全文です。

import React, { useContext } from 'react';
import { FontContext } from './index';

export default function Text() {
  const [value, setValue] = useContext(FontContext);

  const updateHandler = e => {
    setValue({
      ...value,
      [e.target.name]: e.target.value
    });
  };

  return (
    <div>
      <h1 style={{ fontSize: `${value.size}px`, color: `${value.color}` }}>
        FONT SIZE {value.size}px
      </h1>
      <form onSubmit={e => e.preventDefault()}>
        <input
          type="color"
          value={value.color}
          name="color"
          onChange={updateHandler}
        />
      </form>
    </div>
  );
}

Sandboxでコードとプレビューが可能です。

step3

ここまででuseContextを使ったコンテキストの解説は終わりです。余力があれば次に進みリファクタリングを行いましょう。

コンテキストを別コンポーネントに分ける

ここまでのコードをリファクタリングします。役割ごとにコンポーネントを切り分け、コンテキスト自体も汎用的なものにします。

コンテキストをFontContext.jsとして切り分けます。コンテキスト内容は以前と同じ内容ですが、Provider内をprops.childrenとすることで、HOCのような使い方ができるようになります。


// FontContext.js

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

export const FontContext = createContext([
  { size: 0, color: '#FFFFFF' },
  () => {}
]);

export function FontContextProvider(props) {
  const [value, setValue] = useState({
    size: 16,
    color: '#FF0000'
  });

  return (
    <FontContext.Provider value={[value, setValue]}>
      {props.children}
    </FontContext.Provider>
  );
}

テキストサイズを変更するフォームはTextSizeForm.jsxとして切り分けます。


// TextSizeForm.jsx

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

export default function TextSizeForm() {
  const [value, setValue] = useContext(FontContext);

  const updateHandler = e => {
    setValue({
      ...value,
      [e.target.name]: e.target.value
    });
  };

  return (
    <>
      <form onSubmit={e => e.preventDefault()}>
        <input
          type="number"
          name="size"
          value={value.size}
          onChange={updateHandler}
        />
      </form>
    </>
  );
}

同じようにテキストカラーを変更するフォームをTextColorForm.jsxとして切り分けます。先程は色を変更するフォームコンポーネント内に表示されるテキストも配置していましたが、機能を独立させるため取り除いています。


// TextColorForm.jsx

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

export default function TextColorForm() {
  const [value, setValue] = useContext(FontContext);

  const updateHandler = e => {
    setValue({
      ...value,
      [e.target.name]: e.target.value
    });
  };

  return (
    <div>
      <form onSubmit={e => e.preventDefault()}>
        <input
          type="color"
          value={value.color}
          name="color"
          onChange={updateHandler}
        />
      </form>
    </div>
  );
}

表示されるテキストはViewText.jsxとして切り分けました。


// ViewText.jsx

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

export default function ViewText() {
  const [value, setValue] = useContext(FontContext);
  return (
    <div style={{ fontSize: `${value.size}px`, color: `${value.color}` }}>
      <p>Size: {value.size}px</p>
      <p>Color: {value.color}</p>
    </div>
  );
}

最後にindex.jsで3つのコンポーネントを<FontContextProvider>でラップします。


// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { FontContextProvider } from './FontContext';

import TextSizeForm from './TextSizeForm';
import TextColorForm from './TextColorForm';
import ViewText from './ViewText';

function App() {
  return (
    <>
      <FontContextProvider>
        <ViewText />
        <TextColorForm />
        <TextSizeForm />
      </FontContextProvider>
    </>
  );
}

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

上位のコンポーネントとなるindex.jsの見栄えがシンプルになりました。各コンポーネントも独立しており、汎用性を持たすことができます。

step4

コードはSandboxで確認できます。

おわりに

コンテキストはシンプルながら強力です。小さなアプリケーションの場合は、多くのニーズを満たしてくれます。もしコンテキストだけでやっていてパワー不足を感じ始めたら、コンテキスト+useReducerを使った状態管理に挑戦するのをお勧めします。

こちらはReduxライクに状態管理ができ、ステートレスコンポーネントにおけるグローバルな状態管理を行う手段の1つとして定着しているようです。