こんにちは👋🏻、
この記事は、複数のコンポーネント間で状態を管理するためのより効果的な方法を知りたい初心者向けに特別に作成されています。また、コードの保守と理解を困難にする可能性のある、prop ドリルの一般的な問題に対処することも目的としています。まず、コンテキスト API がどのような問題を解決するかから始めましょう。
ビデオ形式の方がお好みの場合は、私の YouTube チャンネルで視聴できるチュートリアルがあります。👇🏻
親コンポーネントから子コンポーネントにデータを渡す必要がある場合があり、その間の多数のコンポーネントを介して props を渡すことになることがあります。これはprop ドリリングと呼ばれ、すぐに面倒なことになります。これを明確にするために、例を見てみましょう。
図に示すように、アプリケーションのルートにあるApp
コンポーネントでデータを取得したとします。ここで、深くネストされたコンポーネント、たとえばGrandchild
コンポーネントがこのデータにアクセスする必要がある場合、通常は、データがGrandchild
に到達する前に、 Parent
とChild
コンポーネントを介して props として渡されます。これは、アプリが大きくなるにつれて厄介な問題になる可能性があります。
もう一つの視覚的表現を次に示します。
上記の例では、 Profile
コンポーネントにはユーザー データが必要ですが、このデータは、中間コンポーネント自体がデータを使用しない場合でも、まずApp
コンポーネントとNavigation
コンポーネントを通過する必要があります。では、これをどのようにクリーンアップすればよいでしょうか。ここで、Context API が役立ちます。
プロップドリル:
React.js の Context API を使用すると、コンポーネント ツリーの各レベルにデータをプロパティとして渡すことなく、コンポーネント間でデータを渡すことができます。これは、コンテキスト オブジェクトで状態を定義するグローバル状態管理システムのように機能し、コンポーネント ツリーのどこからでも簡単にアクセスできます。例を使ってこれを理解しましょう。
図からわかるように、複数のコンポーネントがアクセスするデータを格納するコンテキスト オブジェクトがあります。このデータは、API またはサードパーティのサービスから取得されます。任意のコンポーネントでこのコンテキスト データにアクセスする前に、このデータを必要とするすべてのコンポーネントをコンテキスト プロバイダー コンポーネントにラップする必要があります。
ナビゲーション コンポーネントとプロファイル コンポーネントのデータにのみアクセスする必要がある場合は、アプリ コンポーネントをラップする必要はありません。関連するコンポーネントをContextProvider
でラップすると、コンテキスト データを使用する任意のコンポーネントでコンテキスト データに直接アクセスできます。まだ理解できなくても心配しないでください。コードを調べて、実際に動作を確認しましょう。
まず、 Vite.jsを使用して React アプリを作成しましょう。以下のコマンドをコピーしてプロジェクトをセットアップするだけです。
npm create vite@latest
cd project_name // to change to project directory npm install npm run dev
次に、ブラウザで開発サーバーhttp://localhost:5173
を開くことができます。
まず、必要なフォルダーを作成しましょう。これがプロジェクトのフォルダー構造です。
src | components | context
コンポーネント フォルダーにProfile.jsx
ファイルを作成し、次のコードを追加します。
import React from 'react' const Profile = () => { return ( <div>Profile</div> ) } export default Profile
コンポーネント フォルダーにNavbar.jsx
というコンポーネントをもう 1 つ作成します。
import Profile from './Profile' const Navbar = () => { return ( <nav style={{ display: "flex", justifyContent: "space-between", alignItems: "center", width: "90%", height: "10vh", backgroundColor: theme === "light" ? "#fff" : "#1b1b1b", color: theme === "light" ? "#1b1b1b" : "#fff", border: "1px solid #fff", borderRadius: "5px", padding: "0 20px", marginTop: "40px", }}> <h1>LOGO</h1> <Profile /> </nav> ) } export default Navbar
この<Navbar />
コンポーネントをApp.jsx
ファイルにインポートしましょう。
import Navbar from "./components/Navbar"; function App() { return ( <main style={{ display: "flex", flexDirection: "column", justifyContent: "start", alignItems: "center", height: "100vh", width: "100vw", }} > <Navbar /> </main> ); } export default App;
つまり、基本的に、 <Profile />
コンポーネントは<Navbar />
の子であり、 <Navbar />
<App />
コンポーネントの子です。
context
フォルダーにUserContext.jsx
ファイルを作成しましょう。ファイルに次のコードを追加します。
import { createContext, useEffect, useState } from "react"; export const UserContext = createContext(); export const UserProvider = ({ children }) => { const [user, setUser] = useState(null); const fetchUserData = async (id) => { const response = await fetch( `https://jsonplaceholder.typicode.com/users/${id}` ).then((response) => response.json()); console.log(response); setUser(response); }; useEffect(() => { fetchUserData(1); }, []); return ( <UserContext.Provider value={{ user, fetchUserData }} > {children} </UserContext.Provider> ); };
createContext
を使用して空のUserContext
オブジェクトを作成します。 react
からインポートするようにしてください。コンテキスト オブジェクト内にデフォルト値を追加できますが、今のところは null のままにしておきます。
UserContext.Provider
のように、 UserContext
を使用してプロバイダーを返すUserProvider
を作成します。これは子コンポーネントをラップし、値には子コンポーネントで使用したいものを何でも渡すことができます。
fetchUserData
はid
を受け入れ、その ID を使用してユーザー データを取得します。次に、応答をuser
状態に保存します。
useEffect
でfetchUserData
関数を呼び出しているので、ページの読み込み時に関数が呼び出され、 user
状態にデータが挿入されます。
ここで、このコンテキストを<App />
コンポーネントで使用してみましょう。 <UserProvider />
を使用して<NavBar />
コンポーネントをラップします。次のコードと同じです。
<UserProvider> <Navbar /> </UserProvider>
<Profile />
コンポーネントでuser
状態を使用しましょう。そのためには、 useContext
フックを使用します。これはUserContext
を受け取り、 user
状態やfetchUserData
関数など、 UserProvider
で渡した値を提供します。 <Profile />
コンポーネントは既にプロバイダーでラップされている<Navbar />
コンポーネント内にあるため、ラップする必要がないことに注意してください。
Profile.jsx
を開き、次のコードを追加します。
const { user } = useContext(UserContext); if (user) { return ( <span style={{ fontWeight: "bold", }} > {user.name} </span> ); } else { return <span>Login</span>; }
ここでは、 UserContext
のuser
状態を使用しています。 user
がいる場合はユーザー名を表示し、そうでない場合はログイン メッセージのみを表示します。出力を見ると、navbar コンポーネントにユーザー名があるはずです。これは、任意のコンポーネントのコンテキストにある任意の状態を直接使用する方法です。この状態を使用するコンポーネントは<Provider />
内にラップする必要があります。
複数のコンテキストを使用することもできます。次の例に示すように、プロバイダー コンポーネントを別のプロバイダー コンポーネント内にラップするだけです。
<ThemeProvider> <UserProvider> <Navbar /> </UserProvider> </ThemeProvider>
上記の例では、テーマの状態を管理する<ThemeProvider />
を使用しています。
複数のコンテキスト プロバイダーを使用する完全な例については、上記の YouTube ビデオをご覧ください。
複数のコンポーネントで Context API を使用する場合に発生する問題が 1 つあります。Context API で状態または値が変更されるたびに、すべてのコンポーネントが変更された状態を使用していない場合でも、その特定のコンテキストにサブスクライブされているすべてのコンポーネントが再レンダリングされます。この再レンダリングの問題を理解するために、コンテキストを使用してカウント値を保存および表示する<Counter />
コンポーネントを作成しましょう。
次の例を確認してください。コンポーネント フォルダーにCounter.jsx
ファイルを作成し、次のコードを貼り付けることができます。
import { createContext, memo, useContext, useState } from "react"; const CountContext = createContext(); const CountProvider = ({ children }) => { const [count, setCount] = useState(0); return ( <CountContext.Provider value={{ count, setCount }}> {children} </CountContext.Provider> ); }; function CountTitle() { console.log("This is Count Title component"); return <h1>Counter Title</h1>; } function CountDisplay() { console.log("This is CountDisplay component"); const { count } = useContext(CountContext); return <div>Count: {count}</div>; } function CounterButton() { console.log("This is CounterButton component"); const { count, setCount } = useContext(CountContext); return ( <> <CountTitle /> <CountDisplay /> <button onClick={() => setCount(count + 1)}>Increase</button> </> ); } export default function Counter() { return ( <CountProvider> <CounterButton /> </CountProvider> ); }
上記のコードでは:
CountContext
オブジェクトを作成しますcreateContext.
CountProvider,
カウント値を格納するための状態が 1 つあります。count とsetCount
メソッドcount
value プロパティを通じて子コンポーネントに送信しています。
個々のコンポーネントが何回再レンダリングされるかを確認するために、コンポーネントを個別に作成しました。
<CountTitle />
: このコンポーネントはタイトルのみを表示し、コンテキストからの値も使用しません。
<CountDisplay />
: このコンポーネントはカウント値を表示し、コンテキストからのcount
状態を使用します。
<CounterButton />
: このコンポーネントは、上記のコンポーネントと、 setCount
を使用してカウント値を増やすボタンの両方をレンダリングします。
最後に、他のコンポーネントがカウント値にアクセスできるように、 <CounterButton />
コンポーネントをCountProvider
コンポーネント内にラップします。
ここで、コードを実行してIncrease
ボタンをクリックすると、状態が変化するたびにすべてのコンポーネントが再レンダリングされていることがログに表示されます。 <CountTitle />
は count 値を使用していないにもかかわらず、再レンダリングされています。これは、 <CountTitle />
の親コンポーネントである<CounterButton />
が count の値を使用して更新しているために発生し、再レンダリングされるのです。
この動作を最適化するにはどうすればよいでしょうか。答えはmemo
です。React のmemo
使用すると、プロパティが変更されていない場合にコンポーネントの再レンダリングをスキップできます。 <CountTitle />
コンポーネントの後に、次の行を追加しましょう。
const MemoizedCountTitle = React.memo(CountTitle)
ここで、 <CounterButton />
コンポーネントで<CountTitle />
コンポーネントをレンダリングしているところで、次のコードのように<CountTitle />
を<MemoizedCountTitle />
に置き換えます。
<> <MemoizedCountTitle /> <CountDisplay /> <button onClick={() => setCount(count + 1)}>Increase</button> </>
ここで、カウントを増やしてログを確認すると、 <CountTitle />
コンポーネントがレンダリングされなくなっていることがわかります。
Redux
、より予測可能な状態遷移を伴う複雑な状態管理のための状態管理ライブラリです。一方、Context API は、プロパティ ドリリングなしでコンポーネント ツリーを介してデータを渡すために、シンプルな状態管理用に設計されています。では、どちらを選択すればよいのでしょうか。
状態管理の一般的な選択肢としてもう 1 つライブラリがあります。それはReact Recoil です。
React Recoilについてさらに詳しく知りたい場合は、コメントでお知らせください。皆さんのフィードバックに基づいて、このトピックに関する詳細なチュートリアルを作成します。
React.js
Context API は、複数のコンポーネントにまたがる状態を管理するための強力かつ効率的な方法を提供し、prop ドリルの問題に効果的に対処します。Context API を使用すると、コードを簡素化し、不要な再レンダリングを減らし、アプリケーション全体のパフォーマンスを向上させることができます。
Context API は単純な状態管理に最適ですが、より複雑なアプリケーションではRedux
やReact Recoil
などの他の状態管理ライブラリを使用すると効果的です。これらのツールをいつどのように使用するかを理解することで、より保守性と拡張性に優れた React アプリケーションを構築できるようになります。
この記事を読んでいただきありがとうございます。お役に立てれば幸いです。React、Redux、Next.js を使用したプロジェクトの学習と構築に興味がある場合は、こちらの YouTube チャンネルをご覧ください: CodeBucks
他にも読んでみたい記事がこちらにあります:
私の個人ブログをご覧ください: DevDreaming