「useContextって初学者向けの教材やスクールで習わないから使い方がまずわかりづらい」
「useStateとuseContextの違いがよくわからず、useStateしか使ってこなかった」
「useContextを調べるとよくわからないコンポーネントが書いてありライブラリだと思っていた」
本日はそんな方に向けてuseContextフックを解説していきます。
Reactの魅力はその柔軟性と効率的なステート管理にあります。
その中でもuseContext
フックは、コンポーネント間でデータを共有するための効果的な手段となっています。
この記事では、useContext
が一体何をしているのかを掘り下げ、その背後にあるReactのコンテキストに焦点を当てます。
Reactアプリケーションを開発する上で、コンポーネントツリー全体にまたがるデータの伝達は一般的な課題です。
単純なプロップスの伝播では限界があり、深くネストされたコンポーネントにデータを渡すのは手間がかかります。
ここでuseContext
が威力を発揮します。
useContextフックはReactのコンテキストAPIをベースにしており、親コンポーネントから子孫コンポーネントまでシームレスにデータを受け渡す手段を提供します。
Webにおけるコンテキストは、中核的な機能でありグローバルなデータをコンポーネントツリー内で共有できるようにします。
その動作を理解することで、アプリケーションの状態管理をより効果的に行う手段を手に入れることができます。
ReactのuseContextフックは何をしているのか?
結論、useStateと同じことをしています。
例題としてカウンターアプリをuseStateだけをベースに作ってみます。
import React, { createContext, useState } from 'react';
import ChildComp from './ChildComp';
export const CounterContext = createContext();
const UseContext = () => {
const [count, setCount] = useState(0);
const plusCount = () => {
setCount((prev) => prev + 1);
};
const minusCount = () => {
setCount((prev) => prev - 1);
};
return (
<div>
<button onClick={plusCount}>+</button>
{count}
<button onClick={minusCount}>-</button>
</div>
);
};
export default UseContext;
カウントする数字をuseStateフックを使って初期値0として作りました。
例えば上記の状態からカウントの表記のみ別のコンポーネントに切り分けたいとします。
import React, { useState } from 'react';
import ChildComp from './ChildComp';
export const CounterContext = createContext();
const UseContext = () => {
const [count, setCount] = useState(0);
const plusCount = () => {
setCount((prev) => prev + 1);
};
const minusCount = () => {
setCount((prev) => prev - 1);
};
return (
<div>
<button onClick={plusCount}>+</button>
{/* ここを変更 */}
<ChildComp count={count} />
<button onClick={minusCount}>-</button>
</div>
);
};
export default UseContext;
propsにカウントされた数字が格納されているcountを渡します。
子コンポーネントは以下のようにpropsを受け取る形になります。
import React from 'react';
import { CounterContext } from './UseContext';
const ChildComp = ({ count }) => {
return <div>{count}</div>;
};
export default ChildComp;
これくらいであれば特に問題ないですが実装の規模が大きくなり、今作った子コンポーネント自体にもコンポーネントで切り分けたい部分が出たりすると管理が大変になります。
そこでpropsによるバケツリレーを極力しないようにするためにuseContextフックを使うわけです。
useContextはuseStateと同様にインポート文を書いて使用するものですが、専用のコンポーネントをcreateContextというフックで作成して親コンポーネントをラップします。
createContextを実行した結果を定数に格納すると、その定数自体がコンポーネントとして使用できるようになり以下のように書きます。
// ここを追加
import React, { createContext, useState } from 'react';
import ChildComp from './ChildComp';
// ここを追加
export const CounterContext = createContext();
const UseContext = () => {
const [count, setCount] = useState(0);
const plusCount = () => {
setCount((prev) => prev + 1);
};
const minusCount = () => {
setCount((prev) => prev - 1);
};
return (
// ここを追加
<CounterContext.Provider value={count}>
<div>
<button onClick={plusCount}>+</button>
// ここを修正
<ChildComp />
<button onClick={minusCount}>-</button>
</div>
</CounterContext.Provider>
);
};
export default UseContext;
またcreateContextで作成したコンポーネントにはvalueという属性があり、本来であれば子コンポーネントにpropsとして渡したいデータを格納します。
今回だとcountになるのでvalue=countとします。
あとはコンポーネントをpropsなしで記載する形です。
子コンポーネント側に行きます。
// ここを変更
import React, { useContext } from 'react';
// ここを追加
import { CounterContext } from './UseContext';
const ChildComp = () => {
{/* ここを追加 */}
const count = useContext(CounterContext);
return <div>{count}</div>;
};
export default ChildComp;
propsはもう必要ないので削除して、その代わりにuseContextを使用して先ほどcreateContextで作成したコンポーネントを初期値にした定数countを作ります。
子コンポーネント側で作った定数countをそのまま使用すれば先ほどと同じような動作になります。
propsでやっていた値の受け渡しをuseContextが丸っと引き受けてくれて、仮にコンポーネントが多重になっても各自のコンポーネントでuseContext(CounterContext)とすればカウントを使用できます。
propsでも良いのですがデータの発信場所がわからなくなったり、propsを渡すだけでそのデータを使わないコンポーネントが存在する場合があります。
そのようなわかりづらさや非効率さを解消してくれるのがuseContextになります。
よくあるECサイトのカートシステムでuseContextを使ってみる
続いて別の例としてECサイトのカートシステムをuseContextで作ってみます。
App.jsは以下のようになっていて商品一覧(Products.js)とカート一覧(Cart.js)のページの2ページがあります。
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Products from './screens/useContext/Products';
import Cart from './screens/useContext/Cart';
import './App.css';
function App() {
return (
<>
<BrowserRouter>
<Routes>
<Route path="/" element={<Products />} />
<Route path="/cart" element={<Cart />} />
</Routes>
</BrowserRouter>
</>
);
}
export default App;
続いて商品一覧になるProduct.jsについてですが商品情報のみProductCard.jsという子コンポーネントに切り出す形を作ってみます。
このようにページ=ファイルとはならずに複数のコンポーネントを合体させて1つのページにするのはReactプロジェクトではよくある例です。
また商品データについては定数productListという配列で手書きで作ったものを使用します。
import React from 'react';
import ProductCard from './ProductCard';
const Products = () => {
const productList = [
{
title: 'HTML,CSS',
price: 1000,
},
{
title: 'JavaScript',
price: 1500,
},
{
title: 'Python',
price: 2000,
},
];
return (
<div>
<h1>技術書の一覧</h1>
{productList.map((product, index) => (
<ProductCard title={product.title} price={product.price} key={index} />
))}
</div>
);
};
export default Products;
import React from 'react';
const ProductCard = ({ title, price }) => {
return (
<div>
<div>
<p>{title}</p>
<p>税込{price}円</p>
<button>カートに追加</button>
</div>
<hr />
</div>
);
};
export default ProductCard;
続いてヘッダーメニューをMenu.jsとして作っておき、どのページでも表示するものとします。
import React from 'react';
import { Link } from 'react-router-dom';
const Menu = () => {
return (
<div>
<nav>
<Link to={'/'}>タイトル</Link>
<Link to={'/cart'}>
カート:<span>0</span>
</Link>
</nav>
</div>
);
};
export default Menu;
App.jsにMenu.jsを追加で読み込ませておきます。
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Products from './screens/useContext/Products';
import Cart from './screens/useContext/Cart';
// ここを追加
import Menu from './screens/useContext/Menu';
import './App.css';
function App() {
return (
<>
<BrowserRouter>
{/*ここを追加*/}
<Menu />
<Routes>
<Route path="/" element={<Products />} />
<Route path="/cart" element={<Cart />} />
</Routes>
</BrowserRouter>
</>
);
}
export default App;
「カートに追加」というボタンをクリックしたらカート情報に追加する仕組みを本来であればProductCard.jsに変数や関数を定義するところですが別のファイルでCardContext.jsというものを用意してそちらに書きます。
import { createContext, useState } from 'react';
const CartContext = createContext();
export function CartProvider({ children }) {
const [items, setItems] = useState([]);
const addItems = (title, price) => {
setItems((prev) => [...prev, { title, price }]);
};
return (
<CartContext.Provider value={{ items, addItems }}>
{children}
</CartContext.Provider>
);
}
export default CartContext;
カートに追加された商品一覧をitemsとしてuseStateで初期値を空の配列で管理します。
またカートに追加する処理を関数addItemsとして作成して、配列itemsにオブジェクト形式で追加していく形にしました。
商品一覧のitemsと追加する関数addItemsをvalueというpropsにして全てのコンポーネントで使用するためにはcreateContextでCartContextというコンポーネントを作ります。
CartContextコンポーネントでchildrenを囲むことでApp.js以下の全てのページでitemsとaddItemsが使用できるようになるわけです。
またitemsとaddItemsはvalueというpropsに格納しておき、App.jsに移動して以下のようにします。
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Products from './screens/useContext/Products';
import Cart from './screens/useContext/Cart';
import Menu from './screens/useContext/Menu';
// ここを追加
import { CartProvider } from './screens/useContext/CartContext';
import './App.css';
function App() {
return (
<>
{/* ここを追加 */}
<CartProvider>
<BrowserRouter>
<Menu />
<Routes>
<Route path="/" element={<Products />} />
<Route path="/cart" element={<Cart />} />
</Routes>
</BrowserRouter>
{/* ここを追加 */}
</CartProvider>
</>
);
}
export default App;
それらをもとにカート一覧のCart.jsは以下のように作成できます。
// ここを変更
import React, { useContext } from 'react';
// ここを追加
import CartContext from './CartContext';
const Cart = () => {
const { items } = useContext(CartContext);
return (
<div>
<h1>カート一覧</h1>
{items.map((item, index) => (
<p key={index}>
{item.title}:税込{item.price}円
</p>
))}
</div>
);
};
export default Cart;
useContextフックを使ってCartContextを取得します。
その中で先ほどuseStateで作っていたカート一覧になるitemsを取得します。
itemsは配列でしたのでmapメソッドを使ってカートにある一覧を繰り返し処理で画面表示します。
初期状態はitemsが空の配列なので何も表示されません、カートに追加する処理を追加するためにProductCard.jsに移動します。
// ここを変更
import React, { useContext } from 'react';
// ここを追加
import CartContext from './CartContext';
const ProductCard = ({ title, price }) => {
// ここを追加
const { addItems } = useContext(CartContext);
return (
<div>
<div>
<p>{title}</p>
<p>税込{price}円</p>
{/* ここを変更 */}
<button onClick={() => addItems(title, price)}>カートに追加</button>
</div>
<hr />
</div>
);
};
export default ProductCard;
こちらもCartContextからuseContextフックを使用してaddItemsを取得します。
関数addItemsはbuttonタグのonClickイベントとして追加しました。
またMenu.jsにも現在のカートにある商品数をカウントする部分がありましたので反映させましょう。
// ここを変更
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
// ここを追加
import CartContext from './CartContext';
const Menu = () => {
// ここを追加
const { items } = useContext(CartContext);
return (
<div>
<nav>
<Link to={'/'}>タイトル</Link>
<Link to={'/cart'}>
{/* ここを変更 */}
カート:<span>{items.length}</span>
</Link>
</nav>
</div>
);
};
export default Menu;
Cart.jsと同じでitemsを取得しておきます。
こちらではitemsの中にある商品の個数だけ必要なのでlengthプロパティを指定すれば配列の要素数が取得できて活用できます。
実際の動作を確認してみます。
ボタンをクリックするとMenu.jsのカート個数がカウントされ、カート一覧のページに移動すると追加した商品情報が追加されますね。
もちろんuseStateとpropsを使って同じものを作れますが、ページ数が多くなると同じようなuseStateが増えたりpropsを送る回数が増えて管理が大変になります。
createContextを使った専用のコンポーネントでchildren(App.js以下)をラップしておき、共有する変数や関数を指定しておけば同じような記述を書くことがなくなります。
また作った変数や関数はコンポーネントでuseContextフックを使えば簡単に呼び出すことができるのでpropsでバケツリレーすることが減ります。
試しにMenu.jsでuseContextの結果をコンソールで確認してみましょう。
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import CartContext from './CartContext';
const Menu = () => {
const { items } = useContext(CartContext);
// ここを追加
const state = useContext(CartContext);
console.log(state);
return (
<div>
<nav>
<Link to={'/'}>タイトル</Link>
<Link to={'/cart'}>
{/* ここを変更 */}
カート:<span>{items.length}</span>
</Link>
</nav>
</div>
);
};
export default Menu;
useContextフックで取得したものはオブジェクト形式になっていて中身がitemsとaddItemsというC artContext.jsで作ったものになっていますね。
このようにオブジェクト形式になっていろんな場所でuseContextを使って共通の関数や変数を簡単に呼び出せる仕組みになっていることがわかります。
またオブジェクトですので使用するときには分割代入で短いキーワードにして使用できるのも嬉しい点です。
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import CartContext from './CartContext';
const Menu = () => {
const { items } = useContext(CartContext);
// 元のオブジェクト
const state = useContext(CartContext);
// オブジェクトからプロパティを指定した書き方
const state.items = useContext(CartContext);
// こちらも同じ意味で分割代入しただけ
const { items } = useContext(CartContext);
return (
<div>
<nav>
<Link to={'/'}>タイトル</Link>
<Link to={'/cart'}>
{/* ここを変更 */}
カート:<span>{items.length}</span>
</Link>
</nav>
</div>
);
};
export default Menu;
そもそもコンテキストとは何なのか?
コンテキストとはJavaScriptでもあるキーワードで聞かれたことがあるかもしれません。
もともとはブラウザにJavaScriptが読み込まれたときに関数、変数などを一度スキャンして認識しておく仕組みです。
例えば以下のようなコードがあったとします。
function test() {
console.log('test');
}
test();
こちらをHTMLファイルを介してブラウザに読み込ませるとコンソールにtestと表示されます。
一方で以下のようなコードでも同じことになります。
test();
function test() {
console.log('test');
}
初心者の方だと「それだと順番が逆になるからエラーになるのでは?」と思われるかもしれません。
しかし先ほどお話したコンテキストという仕組みで関数の存在はブラウザに認識してもらっているので好きな場所で実行できるのです。
試しに関数の前後で実行してみましょう。
test();
function test() {
console.log('test');
}
test();
両方とも問題なく動作しています。
これがコンテキストなのですが、そう言うと何でもアリのように誤解される可能性があるので以下のコードも試しておきましょう。
console.log(variable());
const variable = function () {
console.log('test');
};
今度はエラーになり最初に言っていた「順番が逆」と言う意味になっています。
以下の例だとわかりやすいです、もちろんエラーになりますよね。
console.log(student);
let student = 'tanaka';
先ほどのコードはこれと同じことです。
コンテキストは事前に関数や変数をスキャンして認識してくれているものの、あくまで存在を認識しているだけで変数の値まではわかってくれていません。
そもそも変数は後でいくらでも再代入するものなので最初から値まで認識しておく必要がないからです。
つまり変数を以下のように宣言しているのと似ています。
let variable;
上記は宣言のみで値を代入していないのでundefinedと言う状態ですね。
そのため変数に関数を格納している場合には実行する順番と読み込む順番を揃える必要があります。
以下の書き方だとOKです。
const variable = function () {
console.log('test');
};
console.log(variable());
ちなみにコンソールの最後に「undefined」が記載されていますが、こちらは関数の戻り値になりますのでreturnを書くと書き換わる場所になります。
色々と似ているキーワードがありますが考え方をゆっくり理解していくと良いでしょう。