[ Image Source: NASA ]
ReactのContext APIは、アプリケーション全体でGlobal Stateの値を共有することができる強力なツールです。このブログはNext.js(Reactのフレームワーク)で構築していますが、Context APIを利用してStateを管理しています。
Context APIはとても便利なツールで、Hooksと同じくReactプロジェクトに欠かせないものですが、使いこなすことがなかなか難しいように感じます。
Contextについては、公式ドキュメントに詳しい説明付きでテンプレのソースコードが記載されているのですが、いざプロジェクトに適用しようとすると、例えば以下のような問題に直面します。
上記の点に関して、試行錯誤の末、色々なプロジェクトに適用できそうなテンプレートを作成したので、共有したいと思います。
以下、具体的なコードを記載しますが、Githubにソースコードを保存しています 🚀✨
Codesandboxを使って次のようなNext.jsのサンプルアプリケーションを作成しました。
以下、このアプリケーションの中で、Context APIやReact Hooksがどのように用いられているかを説明していきたいと思います。
(プロジェクトのリンクが開きます)
このプロジェクトでは、"myName"と "loading"という二つのStateを定義しています。Stateの定義では、React Hooksの一つであるuseStateを利用しています。
また、myNameの値を変更するためのページ"index.tsx"と、それを表示するだけのページ"display-name.tsx"の二つのページを用意しています。
indexページに表示されている文章"My name is Mike"の名前の部分"Mike(初期値)"が、入力欄に文字列を入力して"Change Name"ボタンをクリックすると、入力した文字列に変更されます。
もう一つのページであるdisplay-nameページでは、myName Stateの値をそのまま表示しています。ここでのポイントは、indexページで行った名前の変更が、display-nameページにも反映されているということです。
このような実装をするためには、myName Stateがプロジェクト全体で利用できる必要があります。
それでは、Stateをプロジェクト内の全てのページで利用する (つまりGlobal Stateとして利用する)にはどうしたらよいでしょうか? このような場合こそ、Context APIの出番ですね。
Context APIについては、オリジナルドキュメントでサンプルコードが公開されていますし、利用方法も様々だと思います。
僕は、試行錯誤する中で、シンプル性 / 汎用性 / 拡張性の観点から、できるだけ多くのプロジェクトに適用できるように、テンプレを作成して利用しています。
具体的には、createContextの独立ファイルを作成 → Contextファイル内でStateを定義 → ProviderでContextから_app.tsxにStateを渡す、というプロセスを通してGlobal Stateを定義する方法を用いています。
import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useMemo, useState } from "react"
export interface ChildrenProps {
children: ReactNode
}
type PageContextType = {
loading: boolean
setLoading: Dispatch <SetStateAction<boolean>>;
myName: string;
setMyName: Dispatch <SetStateAction<string>>;
}
const PageContext = createContext<PageContextType | undefined>(undefined)
export function PageWrapper({ children } : ChildrenProps ) {
const [loading, setLoading] = useState<boolean>(false)
const [myName, setMyName] = useState<string>("Mike")
const pageValue = useMemo(() => ({
loading, setLoading,
myName, setMyName,
}), [
loading, setLoading,
myName, setMyName,
])
return (
<PageContext.Provider value={pageValue}>
{children}
</PageContext.Provider>
);
}
export function usePageContext() {
const context = useContext(PageContext)
if (context === undefined) {
throw new Error('Context is undefined')
}
return context
}
next.jsのメインコンポーネントである_app.tsxファイル(ReactのApp.jsに対応)では、ContextファイルからexportされたPageWrapperを使って
import { AppProps } from 'next/app'
import { PageWrapper } from '../context/PageContext'
import '../styles/globals.css'
function MyApp({ Component, pageProps } : AppProps) {
return (
<>
<PageWrapper>
<Component {...pageProps} />
</PageWrapper>
</>
)
}
export default MyApp
この方法を用いると、Stateの種類に応じて複数のContextファイルを作成することができます。つまり、Contextの種類を自由に増やすことができるという点で、拡張性が比較的高いというメリットがあります
例えば、新しいファイルを用意してUserContextをContext APIを使って作成する場合を考えます。このUserContextでは、userNameやemailAddressなどのStateを定義するとします。このような場合でも、上記の例と同じように、UserWrapperを_app.tsxにexportして、PageWrapperの外側から囲い込むことによって、Global Stateを定義することができます。
このようにして定義されたGlobal Stateは、useContextというReact Hookを用いることによって、各ページファイルの中で利用できます。
まずはindexページのコードを見てみましょう。このページでは、myName Stateの値 (名前)を変更するための入力ウィンドウが設置されています。
import Head from 'next/head'
import Link from 'next/link'
import { usePageContext } from '../context/PageContext'
export default function Home() {
const pageContext = usePageContext()
const {myName} = pageContext
return (
<div className="">
<Head>
<title>React Context Example</title>
<meta name="description" content="React Context Example" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="">
<div className="blue">
<Link href="/change-name">
Change Name
</Link>
</div>
<p>
My name is {myName} 🙋🏻
</p>
</main>
</div>
)
}
次に、display-nameページのソースコードです。indexページと同様に、useContext Hookを用いて、myName Stateを利用して名前を表示しています。
myNameはGlobal Stateですので、indexページでの名前の変更がこのページにも反映されますね。
import Head from 'next/head'
import Link from 'next/link'
import { ChangeEvent, useState } from 'react'
import { usePageContext } from '../context/PageContext'
export default function Home() {
const pageContext = usePageContext()
const {myName, setMyName} = pageContext
const [input, setInput] = useState<string | null>(null);
const onChangeName = (e: ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value)
}
const handleOnclick = () => {
input && setMyName(input)
}
return (
<div className="">
<Head>
<title>React Context Example</title>
<meta name="description" content="React Context Example" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="">
<div className="blue">
<Link href="/">
HOME
</Link>
</div>
<div>
<input type="text" onChange={onChangeName} required />
</div>
<div>
<button onClick={handleOnclick} className="">
Change Name
</button>
</div>
<div>
<button onClick={() => setMyName("Mike")} className="">
Reset
</button>
</div>
<p>
My name is <span className="red">{myName}</span> 🙋🏻
</p>
</main>
</div>
)
}
以上、ReactのContext API + React Hooksを使った Global Stateの定義の仕方について、Next.jsのサンプルプロジェクトの実例を用いつつ、私なりの利用方法をご説明しました。良ければご参考ください😉
Context APIを用いることによって、プロジェクト全体で利用できるGlobal Stateを比較的簡単に定義できます。そしてGlobal Stateを手軽に利用できると、プロジェクトで実装できる機能の幅もグンと広がるのではないでしょうか?
また、Context APIの利用方法について、もっと効率的な方法をご存知の場合は、ぜひ教えていただけると嬉しいです🙇🏻♂️