React Context APIを理解する
ここしばらくフロントの勉強が疎かになっており、会社での立場に危機感(?)を感じたので、現実から逃げずにちゃんと勉強していくことにしました。
今回は 下記公式ドキュメントを元に Context API について理解したいと思います。
Context APIの使い所
子コンポーネントにデータを渡したい時は props として渡すのが基本ですが、コンポーネントの階層が深くなるほど、props のバケツリレーが多くなり、コードの見通しが悪くなったり面テンスがしにくくなってしまいます。
それを回避するために Redux などの状態管理ライブラリ を使って storeを作りそこから必要なデータを props として任意のコンポーネントで受け取ることができるようにしていました。
そのような仕組みをReactの組み込みAPIでできるようにしたのが、この Context API です。
かといってContext API を使えば Redux を使わずに済む というわけでもなさそうです。
そのあたりについては下記記事が参考になります。
- ReactのNew Context APIは便利だけどreduxを使うのはやめないと思った | WEB EGG
- React v16で実装された new Context APIを使って、Reduxへ別れを告げる - Qiita
とはいえ簡単なアプリケーションやプロトタイプ実装には有用な機能だと思うので、簡単なサンプルを作って試してみることにします。
サンプル完成版
今回は下記のようなサンプルを作りました。 公式ドキュメントに則って、テーマ、ユーザー情報をグローバルな情報として持ち、それぞれ切り替える処理を入れています。
コンポーネントの階層
コード全体
import React from 'react'; import ReactDOM from 'react-dom'; // contextの作成(デフォルト値セット) const RootContext = React.createContext({ theme: 'light', user: { name: 'ゲスト' }, toggleTheme: () => {}, setUser: () => {} }); class App extends React.Component { constructor(props) { super(props); // bind this.toggleTheme = this.toggleTheme.bind(this); this.setUser = this.setUser.bind(this); // state初期設定 this.state = { theme: 'light', user: { name: 'ゲスト' }, toggleTheme: this.toggleTheme, setUser: this.setUser }; } /** * テーマ切り替え処理 */ toggleTheme() { this.setState(state => ({ theme: state.theme === 'light' ? 'dark' : 'light' })); } /** * ユーザー情報設定処理 * @param {object}} userData */ setUser(userData) { this.setState({ user: userData }); } render() { return ( // Provider に値セット <RootContext.Provider value={this.state}> <Layout /> </RootContext.Provider> ); } } function Layout() { return ( <RootContext.Consumer> {/* Provider から受け取ったテーマをもとにスタイリング */} {({theme}) => ( <div style={{ display: 'flex', flexDirection: 'row-reverse', justifyContent: 'flex-end', backgroundColor: theme === 'light' ? "#fff" : '#222', color: theme === 'light' ? "#222" : '#fff' }}> <Main /> <Sidebar /> </div> )} </RootContext.Consumer> ); } function Main() { return ( <main> <h2>メインコンテンツ</h2> <div> <ThemeToggleButton /> <UserForm /> </div> </main> ); } function ThemeToggleButton() { return ( <RootContext.Consumer> {/* Provider から受け取った関数をリスナ登録 */} {({toggleTheme}) => <button onClick={toggleTheme}>テーマチェンジ</button>} </RootContext.Consumer> ); } function UserForm() { const handleSubmit = (setUser, e) => { e.preventDefault(); setUser({ name: e.target.name.value }); } return ( <RootContext.Consumer> {/* Provider から受け取った関数をリスナ登録 */} {({setUser}) => ( <form onSubmit={handleSubmit.bind(this, setUser)}> <div>氏名:<input type="text" name="name" required/></div> <div><input type="submit" value="送信" /></div> </form> )} </RootContext.Consumer> ) } function Sidebar() { return ( <RootContext.Consumer> {({user}) => ( <aside style={{ paddingRight: 30, marginRight: 30, borderRight: '1px solid #ccc' }}> <h2>サイドバー</h2> <p> {/* Provider から受け取ったuser情報をもとに表示 */} こんにちは {user.name} さん! </p> </aside> )} </RootContext.Consumer> ) } ReactDOM.render(<App />, document.getElementById('root'));
コンテキストの作成
まず、コンテキストオブジェクトを作成します。
// contextの作成(デフォルト値セット) const RootContext = React.createContext({ theme: 'light', user: { name: 'ゲスト' }, toggleTheme: () => {}, setUser: () => {} });
こちらでは React.createContext
メソッドの引数に デフォルト値を設定していますが、特になくても問題はありません。
defaultValue 引数は、コンポーネントがツリー内の上位に一致するプロバイダを持っていない場合のみ使用されます。これは、ラップしない単独でのコンポーネントのテストにて役に立ちます。補足: undefined をプロバイダの値として渡しても、コンシューマコンポーネントが defaultValue を使用することはありません。
https://ja.reactjs.org/docs/context.html#reactcreatecontext より
たとえば、App.render()
内の provider の記述を削除してみます。
- <RootContext.Provider value={this.state}> <Layout /> - </RootContext.Provider>
これで再読み込みしてもアプリケーションの表示は問題ありません。デフォルト値が設定されているからです。テーマチェンジ等は空の関数が渡されているのでボタンをクリックしても何も起きません。
この状態でデフォルト値を削除してしまうともちろんエラーとなります。
Provider コンポーネントでラップする
次に Provider
コンポーネントでコンポーネントをラップします。
これによりラップされた子孫コンポーネント(正確にはコンシューマコンポーネント)が Provider の value
に渡された値をいつでも参照できるようになります。
class App extends React.Component { // 略 render() { return ( // Provider に値セット <RootContext.Provider value={this.state}> <Layout /> </RootContext.Provider> ); }
value に直接オブエジェクトを渡すと問題がある
Provider の子孫のコンシューマコンポーネントは、 value
の値が変更するたびに再レンダーされます(souldComponentUpdate
メソッドの影響を受けずに更新される)。
value
に渡された値は Object.is
と同じアルゴリズムで前後の値が比較されるため、オブジェクトを直接 value
に渡すと毎回再レンダーされる問題があります。
console.log(Object.is({a:1}, {a:1})); // false const a = { a: 1 }; console.log(Object.is(a, a)); // true console.log(Object.is(a, {a:1})); // false
このような背景から、value
には this.state
を渡しています。
Provider の value は上書きできる
公式ドキュメントでは下記のような説明があります。
プロバイダは値を上書きするために、ツリー内のより深い位置でネストできます。
これを使って、ユーザー氏名を登録した値を大文字に変換して上書きしてみます。
function Layout() { return ( <RootContext.Consumer> {/* Provider から受け取ったテーマをもとにスタイリング */} - {({theme}) => ( + {({theme, user}) => ( <div style={{ // 略 }}> <Main /> - <Sidebar /> + <RootContext.Provider value={{ user: { name: user.name.toUpperCase()}}}> + <Sidebar /> + </RootContext.Provider> </div> )} </RootContext.Consumer> ); }
直近の Provider
コンポーネントの value を見るので、Sideber
コンポーネントはこの大文字に変換された user.name を表示します。
Consumer コンポーネントで Provider の value を参照する
Consumer コンポーネントとは Provider コンポーネントの子孫コンポーネントにあたり、コンテキストの変更を購読できます。
Consumer コンポーネントでは、value
をもとに React.Node を返す関数を記述します。
<MyContext.Consumer> {value => /* コンテクストの値に基づいて何かをレンダーします */} </MyContext.Consumer>
このコンポーネントを使うことで関数コンポーネントでコンテキストを購読することができます。
逆にこのコンポーネントを使わない場合は、クラスコンポーネントにして、Class.contextType
を使う必要があります。
- function Sidebar() { + class Sidebar extends React.Component { + static contextType = RootContext; + render() { return ( - <RootContext.Consumer> - {({user}) => ( <aside style={{ // 略 }}> <h2>サイドバー</h2> <p> {/* Provider から受け取ったuser情報をもとに表示 */} - こんにちは {user.name} さん! + こんにちは {this.context.user.name} さん! </p> </aside> - )} - </RootContext.Consumer> - ) + ); + } }
クラスコンポーネント内でも、Consumer コンポーネントは使えます。
そのためコンテキストを使いたいから、関数 or クラスコンポーネントにする、という考えは不要です。
Class.contextType
は どんなライフサイクルメソッド内でも使えます。詳細は下記参照。
https://ja.reactjs.org/docs/context.html#classcontexttype
Context.displayName を使って見やすくできる
サンプルでは使っていませんが、作成したコンテキストオブジェクトのdisplayNameプロパティに任意の文字列をセットすると、React DevTools でその文字列が表示され見やすくなります。
RootContext.displayName = 'RootContext';
コンテキストは複数作成することも可能なので、複数作成するときは displayName を設定しておいたほうがデバッグがしやすそうです。
まとめ、感想
実際に使ってみると、思っていたよりも簡単に使うことができて素直に良いなと感じました。
シンプルな分たくさん Provider
が乱立すると大変なことになりそうなので、そのあたりは注意が必要そうです。
すでに redux を取り入れている場合や併用する場合はきちんとそれぞれの役割や責任を明確化しておくことが必要かもしれません。
Hooks と組み合わせるとよりスケールアップできるとのことなので、そのあたりはまた Hooks を学ぶ際に試したいと思います。