こんなに簡単なの?React Hook useContextでデータ共有
ReactのuseContextはコンポーネント間でのデータ(状態)の共有とデータ(状態)の受け渡しに関するHookです。useStateやuseReducerと組み合わせて利用することができます。まず本文書では最も基本的なuseContextの使用方法について説明を行い、その後にuseStateとuseReducerを使った利用方法を別々に説明します。その後コンポーネントのRe-renderについても動作確認を行い、本文書を読み終えるとuseContextがどのようなものかを理解することができます。
React 18で動作確認を行なっています。
目次
useContextとは
通常親コンポーネントから子コンポーネントにデータを渡す際はpropsを介して行います。しかし親から子、そのまた子といったように複数のコンポーネントを介してデータを渡す場合にprops(prop-drilling)では設定が複雑になってきます。
ReactのContext APIを利用することでpropsを利用することなく異なる階層のコンポーネントとデータの共有を行うことができます。propsに比べて覚えなければいけないことがいくつかあるので慣れるまではすべて暗記しようとせず実際に利用しないといけない時に本文書を読み直して復習してください。
最も簡単な使用例
App.jsファイルを一番親のコンポーネントとして、App.jsファイルに子コンポーネントAをimportし、コンポーネントAにコンポーネントB、コンポーネントBにコンポーネントCをimportした4階層のコンポーネントを作成します。
一番上の親コンポーネントであるApp.jsのファイルの中身は下記のようになります。ComponentA、B、Cについてはcomponentsディレクトリを作成してその下に作成していきます。
import ComponentA from './components/ComponentA'
function App() {
return (
<div style={{ textAlign: 'center' }}>
<h1>Learn useContext</h1>
<ComponentA/>
</div>
);
}
export default App;
ComponentA
import ComponentB from './ComponentB'
const ComponentA = () => {
return (
<div>
<p>Component A</p>
<ComponentB />
</div>
)
}
export default ComponentA
ComponentB
import ComponentC from './ComponentC'
const ComponentB = () => {
return (
<div>
<p>Component B</p>
<ComponentC />
</div>
)
}
export default ComponentB
ComponentC
const ComponentC = () => {
return (
<div>
<p>Component C</p>
</div>
)
}
export default ComponentC
ブラウザで見ると下記のように表示されます。
準備ができたのでApp.jsから数字100をContext APIを利用してComponentCに渡します。
親コンポーネントでの設定
まずApp.jsでContextの作成を行います。Contextオブジェクトの作成はcreateContextで行います。Contextではデータを渡す側をProviderと呼びデータを受け取る側をConsumerと呼びます。
export const UserCount = createContext()
作成したUserCountをexportしているのはContextを利用するComponentCを含めた他のコンポーネントでimportを行なって利用するためです。
次はUserCount.Providerコンポーネントで数字を渡したいコンポーネントが入っているComponentAを囲みます。UserCount.Providerのpropsのvalueに100を設定します。UserCount.Providerというタグでわざわざ囲まないといけないという処理が必要ですがこれだけで親コンポーネントでの設定は完了です。ここまでの設定ではブラウザ上には何の変化もありません。
import { createContext } from 'react';
import ComponentA from './components/ComponentA'
export const UserCount = createContext()
function App() {
return (
<div style={{ textAlign: 'center' }}>
<h1>Learn useContext</h1>
<UserCount.Provider value={100}>
<ComponentA/>
</UserCount.Provider>
</div>
);
}
export default App;
値を受け取るコンポーネントでの設定
今回はComponentCで値を受け取るのでComponenCのみ設定を行います。ComponentBなど他のコンポーネントでも値を受け取りたい場合に行う設定の設定はすべて同じです。
ComponentCでuseContext Hookを利用するためimportします。また、App.jsでexportしたUserCountはここでimportする必要があります。
import {useContext} from 'react'
import { UserCount } from '../App'
useContextとUserCountを使ってvalueで設定した値を取り出し変数countに入れます。
const count = useContext(UserCount)
これでcountをコンポーネントCで利用することができます。
import { useContext} from 'react'
import { UserCount } from '../App'
const ComponentC = () => {
const count = useContext(UserCount)
return (
<div>
<p>Component C</p>
<p>{count}</p>
</div>
)
}
export default ComponentC
ブウウザで確認し、Component Cの文字列の下に100が表示されれば、App.jsで設定した値がComponent Cに渡されたことになります。
これがContext, useContextを使った最もシンプルな例です。App.jsで設定した値をpropsのようにComponentA, Bを介することなくComponentCで受け取ることができました。
useContextを利用しない場合
必ずしもuseContextを利用する必要はなく下記のようにUseCount.Consumerを利用することでComponent CでApp.jsで設定した値を表示することができます。useContext hookを利用している時はConsumerという言葉は意識しませんが利用しない場合はConsumerを利用するのでデータを与えるProviderとデータを受け取るConsumerがはっきりと区別できます。
import { UserCount } from '../App';
const ComponentC = () => {
return (
<UserCount.Consumer>
{(count) => {
return <p>{count}</p>;
}}
</UserCount.Consumer>
);
};
export default ComponentC;
useStateと一緒に利用する
先程は100という値だけuseContextを利用してComponentCに渡しましたが今回はuseStateと一緒に利用します。
App.jsでuseStateをimportとして、useStateで設定したcountとsetCountをvalueに設定します。countの初期値は100に設定しています。
import { createContext, useState } from 'react';
import './App.css';
import ComponentA from './components/ComponentA';
export const UserCount = createContext();
function App() {
const [count, setCount] = useState(100);
const value = {
count,
setCount,
};
return (
<div className="App">
<h1>Learn useContext</h1>
<UserCount.Provider value={value}>
<ComponentA />
</UserCount.Provider>
</div>
);
}
export default App;
ComponentCで先程と同様の方法で受け取ります。
import {useContext} from 'react'
import { UserCount } from '../App'
const ComponentC = () => {
const { count, setCount } = useContext(UserCount);
return (
<div>
<p>Component C</p>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}
export default ComponentC
受け取ったsetCountを利用してonClickイベントを利用してcountの値を増やします。
ボタンをクリックするとcount数字が1ずつ増えていくことが確認できます。ただの値だけではなくuseStateの値と関数もuseContextを利用してコンポーネント間で共有できることがわかりました。簡単ですね。
useReducerと一緒に利用する
useReducerはuseStateと同様に状態の管理を行うことができるのでuseContextと一緒に利用することも可能です。useStateでもuseReducerでも渡したい値をProviderのvalueに設定するので違いはなく簡単です。
useReducerを使いなれていない人にとってはuseStateほど設定方法がシンプルではないので先にuseReducerの設定方法を理解しておく必要があります。
useStateの時はcountとsetCountをvalueで渡していましたが、useReducerの場合はstateとdispatchを渡します。countとstateは現在の状態を保持し、setCountとdispatchはどちらも値の更新を行う関数です。actionにINCREMENTを設定しているのでactionがINCREMENTの場合はcountの値を1増やし、それ以外のACTIONの場合はcountの値を1減らします。
import React, { useReducer } from 'react';
import ComponentA from './components/ComponentA';
export const UserCount = React.createContext();
const initialState = {
count: 100,
};
function App() {
const reducer = (state, action) => {
if (action === 'INCREMENT') {
return { count: state.count + 1 };
} else {
return { count: state.count - 1 };
}
};
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div style={{ textAlign: 'center' }}>
<UserCount.Provider value={{ state, dispatch }}>
<h1>Learn useContext</h1>
<ComponentA />
</UserCount.Provider>
</div>
);
}
export default App;
ComponentCではcountとsetCountの代わりにstateとdispatchを利用します。dispatchを利用するためにはACTIONを引数に設定する必要があります。先程設定したINCREMENTを設定しています。
import React, { useContext } from 'react';
import { UserCount } from '../App';
const ComponentC = () => {
const { state, dispatch } = useContext(UserCount);
return (
<div>
<p>Component C</p>
<p>{state.count}</p>
<button onClick={() => dispatch('INCREMENT')}>+</button>
</div>
);
};
export default ComponentC;
ブラウザ上での操作はuseStateを利用した時と変わりません。
Context用のコンポーネントを作成
先ほどはApp.jsファイルの中でcreateContextを実行していましたがより汎用的にするためにContext用のコンポーネントの作成を行います。コードも最初は正直わかりにくいとは思いますが先ほど記述したコードと比較しながら確認していけばどのような処理が行われているか理解できるかと思います。useStateを利用していますがuseReducerでも設定方法は同じです。
srcフォルダの下にcontextフォルダを作成します。フォルダが作成できたらCountContext.jsファイルを作成します。
CountContext.jsファイルの中にはコンポーネント間で共有したいデータ、関数を記述します。
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>
);
}
他のコンポーネントでuseCountContextとCountProviderをimportできるようにexport functionで作成しています。
ContentContext.jsファイルの中でContextに関する処理をほとんど記述しているので他のコンポーネントでの記述量がほとんどありません。
App.jsファイルではCountContextコンポーネントからCountProvider関数をimportします。
import React from 'react';
import './App.css';
import ComponentA from './components/ComponentA.js';
import { CountProvider } from './context/CountContext';
function App() {
return (
<div className="App">
<h1>Learn useContext</h1>
<CountProvider>
<ComponentA />
</CountProvider>
</div>
);
}
export default App;
ComponentCコンポーネントではCountContextコンポーネントからuseCountContext関数をimportします。useContextはuseCountContextの中で使われているでComponentCでimportする必要はありません。
import React from 'react';
import { useCountContext } from '../context/CountContext';
const ComponentC = () => {
const { count, setCount } = useCountContext();
return (
<div>
<p>Component C</p>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
};
export default ComponentC;
ブラウザ上での表示に変化はありません。
Re-renderの確認
ReactではコンポーネントのRe-renderがパフォーマンスに影響を与える場合があるのでContext APIではどのような状況でRe-renderが行われるのか確認を行います。
Component CでCountの値を更新した場合に上の階層のComponent AでRe-renderが行われるか確認を行います。
import ComponentB from './ComponentB';
const ComponentA = () => {
console.log('ComponentA Re-Render');
return (
<div>
<p>Component A</p>
<ComponentB />
</div>
);
};
export default ComponentA;
ページを開いた直後にはコンソールに”ComponentA Re-Render”が表示されますがボタンをクリックしてCountを増やしてもRe-renderされることはありません。useContextを利用していないコンポーネントではRe-renderは行われないことがわかります。
useContextを設定するとRe-renderが発生します。returnの中でcount, setCountを利用しているかどうかは関係ありません。下記はuseCountContextからcountとsetCountを取得して利用はしていません。
import { useCountContext } from '../context/CountContext';
import ComponentB from './ComponentB';
const ComponentA = () => {
console.log('ComponentA Re-Render');
const { count, setCount } = useCountContext();
return (
<div>
<p>Component A</p>
<ComponentB />
</div>
);
};
export default ComponentA;
countを利用した場合はRe-renderが行われることは理解できるので、setCountの関数のみを取り出した場合にも再描写が発生するか確認します。動作確認するとsetCountだけでも再描写されることがわかりました。
import { useCountContext } from '../context/CountContext';
import ComponentB from './ComponentB';
const ComponentA = () => {
console.log('ComponentA Re-Render');
const { setCount } = useCountContext();
return (
<div>
<p>Component A</p>
<ComponentB />
</div>
);
};
export default ComponentA;
動作確認からContext APIではuseContextを利用するコンポーネント(Consumerコンポーネント)はすべてデータ(状態)の更新が行われるとRe-renderされることがわかります。頻繁に更新が行われ複数のコンポーネントでuseContextが設定されている場合はコンポーネントも頻繁にRe-renderされることを知っておく必要があります。
他のコンポーネントでも表示
ComponentA, ComponentBでもデータが共有できているのか確認するためにComponentAではcountを表示できるように変更します。setCountは必要ないので必要なcountのみ利用します。
import React from 'react';
import ComponentB from './ComponentB';
import { useCountContext } from '../context/CountContext';
const ComponentA = () => {
const { count } = useCountContext();
return (
<div>
<p>Component A</p>
<ComponentB />
<p>{count}</p>
</div>
);
};
export default ComponentA;
ComponentBではComponentCと同様にボタンを追加し、ボタンをクリックするとcountが増える設定を追加します。
import React from 'react';
import ComponentC from './ComponentC';
import { useCountContext } from '../context/CountContext';
const ComponentB = () => {
const { count, setCount } = useCountContext();
return (
<div>
<p>Component B</p>
<button onClick={() => setCount(count + 1)}>+</button>
<ComponentC />
</div>
);
};
export default ComponentB;
ComponentCでは先ほどはcountを表示させていましたがCompnentAで表示させるため削除しています。ComponentAで引き続きcountを表示させても問題はありません。
import React from 'react';
import { useCountContext } from '../context/CountContext';
const ComponentC = () => {
const { count, setCount } = useCountContext();
return (
<div>
<p>Component C</p>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
};
export default ComponentC;
どちらのボタンを押してもcount数はアップします。
関数を追加
count数を減らせるconuntDown関数をCountContext.jsファイルに追加して共有を行い、ComponentCで実行できるように変更を行います。
追加したcountDown関数は他のコンポーネントで利用するためにはvalueオブジェクトの中に追加する必要があります。
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 countDown = () => {
setCount(count - 1);
};
const value = {
count,
setCount,
countDown,
};
return (
<CountContext.Provider value={value}>{children}</CountContext.Provider>
);
}
これでcountDown関数が他のコンポーネントでも利用可能になったのでComponentCを以下のように更新します。
import React from 'react';
import { useCountContext } from '../context/CountContext';
const ComponentC = () => {
const { countDown } = useCountContext();
return (
<div>
<p>Component C</p>
<button onClick={countDown}>-</button>
</div>
);
};
export default ComponentC;
ブラウザで確認すると”+”ボタンをクリックするとCountが増え、”-”ボタンをクリックするとCountが減ります。
Context用のコンポーネントを作成した場合は処理はすべてCountContext.jsに追加するだけだけで他のコンポーネントの影響はありません。他のコンポーネントで追加した処理を利用した場合はuseCountContextから取り出す際に追加した関数を指定するだけです。
複数のContextの設定
Context APIのContextは1つではなく複数設定することも可能です。Contextを複数設定した場合の利用方法について確認します。
contextフォルダに新たにAnotherCountContext.jsファイルを作成します。中身はこれまでに利用してきたCountContext.jsファイルと同じで変数の名前だけ変えています。通常は機能や役割によって異なるProviderの作成を行います。
import { createContext, useState, useContext } from 'react';
const AnotherCountContext = createContext();
export function useAnotherCountContext() {
return useContext(AnotherCountContext);
}
export function AnotherCountProvider({ children }) {
const [anotherCount, setAnotherCount] = useState(200);
const value = {
anotherCount,
setAnotherCount,
};
return (
<AnotherCountContext.Provider value={value}>
{children}
</AnotherCountContext.Provider>
);
}
作成したAnotherCountProviderをimportしてApp.jsファイルで以下のように設定を行います。CountProviderとAnotherCountProviderの2つのコンポーネントをComponentAにラップします。CountProviderとAnotherCountProviderは逆でも構いません。
import './App.css';
import ComponentA from './components/ComponentA';
import { CountProvider } from './context/CountContext';
import { AnotherCountProvider } from './context/AnotherCountContext';
function App() {
return (
<div className="App">
<h1>Learn useContext</h1>
<CountProvider>
<AnotherCountProvider>
<ComponentA />
</AnotherCountProvider>
</CountProvider>
</div>
);
}
export default App;
ComponentCで共有したデータと関数を利用します。
import { useAnotherCountContext } from '../context/AnotherCountContext';
import { useCountContext } from '../context/CountContext';
const ComponentC = () => {
const { count, setCount } = useCountContext();
const { anotherCount, setAnotherCount } = useAnotherCountContext();
return (
<div>
<p>Component C</p>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>+</button>
<p>another count: {anotherCount}</p>
<button onClick={() => setAnotherCount(anotherCount + 1)}>+</button>
</div>
);
};
export default ComponentC;
それぞれのカウンターは独立しているのでボタンをクリックするともう一つのカウンターに影響を与えることなくカウント数が増えます。
1つのContextにまとめることができますが複数の共有データを1つのContext APIでまとめるとあるConsumerコンポーネントでは利用することがないデータが更新されても一緒に再描写されることになります。複数のContextにわけることで無駄な再描写が減少するだけでなくシンプルなコードで記述することもできます。
リロード後にデータを保持したい場合(Local Storage)
Context APIに限定されたことではありませんがページをリロードすると更新したデータは初期値に戻ります。ページがリロードしてもデータが保持できるようにブラウザのローカルストレージを利用します。
hooksフォルダを作成してHook用のファイルuseLocalStorage.jsファイルを作成します。useLocalStorangeでは引数にkey, initialValueを受け取ります。ローカルストレージはkey, valueの形で保存することができ引数のkeyはローカルストレージに保存する際に利用します。useStateで初期値を設定していますがローカルストレージにkeyを持つデータが保存されているか確認を行い保存されている場合は初期値にローカルストレージから取得したvalueを設定し、保存されていない場合は渡されたinitialValueを設定します。
useEffectを利用してkeyまたはvalueに更新があった場合にローカルストレージに更新した値を保存しています。
import { useEffect, useState } from 'react';
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
const keyValue = localStorage.getItem(key);
return keyValue ? JSON.parse(keyValue) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
export default useLocalStorage;
CountContex.jsファイルで作成したHookのuseLocalStorageをimportして引数のkeyにcount, 初期値に100を設定しています。その他のコードに変更はありません。useContextを利用するConsumerコンポーネントのComponentCも変更はありません。
import { createContext, useContext } from 'react';
import useLocalStorage from '../hooks/useLocalStorage';
const CountContext = createContext();
export function useCountContext() {
return useContext(CountContext);
}
export function CountProvider({ children }) {
const [count, setCount] = useLocalStorage('count', 100);
const value = {
count,
setCount,
};
return (
<CountContext.Provider value={value}>{children}</CountContext.Provider>
);
}
設定後”+”ボタンをクリックしてカウントをアップした後ページを利用しても値が保持されている場合は正常に動作しています。
ブラウザのデベロッパーツールのアプリケーションのローカルストレージを確認するとキーにcount、値にcountの値が保存されています。ボタンをクリックすると値も増えます。
まとめ
useContextの設定方法については前半部分は簡単だと感じてもらえたかもしれませんが後半部分は少し難しく感じられたかもしれません。しかし実際に動作確認してみると思っていたほど難しくないことを理解してもらえたのではないでしょうか。