コンテクスト – React
コンテクストは React の簡易なグローバル管理機能です。 props を経由せずに値を操作、反映できることによって多様な props のリレーを行うことなく各所で同一の値を扱えます。これを利用することで props リレーをすることなく関数一つで通知なりなんなりを呼び出せます。
デモは次です。
各ボタンコンポーネントでは props を用いていませんが、ボタンを押すとグローバル領域の通知用スナックバーに通知が追加されます。呼び出し側のソースコードは次です。
// 通知追加ボタンコンポーネント クラスコンポーネント版
import React from "react";
import { NotifyMessagesContext } from "./NotifyMessagesContext.jsx";
export default class ClassComponent extends React.Component {
constructor(props, context) {
// context を元クラスである React.Component に渡すとよしなにやってくれます
super(props, context);
this.pushMessage = () => {
// 実行はこの context に引っ付いた関数を呼び出すのみ
this.context.pushMessage("クラスコンポーネントからの通知");
};
}
render() {
return (
<div>
<button className="push-notify-btn" onClick={this.pushMessage}>
通知追加 in クラスコンポーネント
</button>
</div>
);
}
}
// どの context を使うかの宣言。これがないとクラスコンポーネントはコンテクストを使えません
ClassComponent.contextType = NotifyMessagesContext;
// 通知追加ボタンコンポーネント 関数コンポーネント版
import React from "react";
import { useNotifyMessages } from "./NotifyMessagesContext.jsx";
export default function FunctionComponent() {
// import した useNotifyMessages から pushMessage を呼び出し
const { pushMessage } = useNotifyMessages();
// ボタンを押したら pushMessage を文字列を引数にして呼び出すだけ
return (
<div>
<button
className="push-notify-btn"
onClick={() => pushMessage("関数コンポーネントからの通知")}
>
通知追加 in 関数コンポーネント
</button>
</div>
);
}
呼び出し側はコンテクストから一つの関数を呼び出し、それを実行しているのみです。グローバルなところから呼び出すため props のリレーも不要です。これだけで立派な通知になるので再利用が容易です。
これを実現しているコンテクストのコードは次です。 Matterial UI のスナックバーを用いていますが、原理的には他のコンポーネントでも可能です。
Snackbar API – Material-UI
import React, { useContext, useState } from "react";
import Snackbar from "@material-ui/core/Snackbar";
import IconButton from "@material-ui/core/IconButton";
import CloseIcon from "@material-ui/icons/Close";
// どの様なコンテクストかを定義.
// キーを持つメッセージ群 と メッセージを追加する関数
export const NotifyMessagesContext = React.createContext({
messages: {},
pushMessage: msg => {}
});
// 各関数コンポーネントで使う
export const useNotifyMessages = () => useContext(NotifyMessagesContext);
// ルートコンポーネントで使う
export const NotifyMessagesProvider = NotifyMessagesComponent;
// メッセージのインデックス。関数コンポーネントでは保持できないので外部に定義
let msgIndex = 0;
/**
* スナックバーに送られてきたメッセージを表示する
* @constructor
*/
function NotifyMessagesComponent(props) {
// メッセージを state で持つ
const [messages, setMessages] = useState({});
// メッセージの追加関数
const pushMessage = newMessage => {
const newMsgIndex = msgIndex + 1;
// 送られてきたメッセージに時刻の接頭辞をつける
const formattedNewMessages = `[${new Date().toISOString()}]:${newMessage}`;
// state にメッセージを追加
setMessages({ ...messages, [`${newMsgIndex}`]: formattedNewMessages });
msgIndex++;
};
// メッセージを削除する関数
const deleteMessage = (deleteKey, reason = "") => {
if (reason === "clickaway") {
// スナックバーの外をクリックした時は閉じない
return;
}
const newMessages = {};
// 削除対象以外のキーを持つメッセージを残した部分を newMessages に入れて state にセット
Object.keys(messages)
.filter(msgKey => msgKey !== deleteKey)
.forEach(msgKey => (newMessages[msgKey] = messages[msgKey]));
setMessages(newMessages);
};
// スナックバーを連続して表示する部分. 5000ms で自動で消える
// NotifyMessagesContext.Provider と props.children が重要。他は Material UI関連の定義
return (
<NotifyMessagesContext.Provider value={{ messages, pushMessage }}>
{Object.keys(messages).map((msgKey, index) => (
<Snackbar
key={msgKey}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
style={{ bottom: `${24 + 72 * index}px` }}
autoHideDuration={5000}
open={!!messages[msgKey]}
onClose={(event, reason) => deleteMessage(msgKey, reason)}
message={messages[msgKey]}
action={
<IconButton
size="small"
aria-label="close"
color="inherit"
onClick={() => deleteMessage(msgKey)}
>
<CloseIcon fontSize="small" />
</IconButton>
}
/>
))}
{props.children}
</NotifyMessagesContext.Provider>
);
}
コンテクストのプロバイダで囲った中で props.children を呼びます。こうするとこのコンポーネントで囲った中ではどこでもコンテクストが使えます。このコンポーネントを呼び出すのは大体、ルートコンポーネントでこれは次のコードです。
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { NotifyMessagesProvider } from "./NotifyMessagesContext";
const rootElement = document.getElementById("root");
// import した NotifyMessagesProvider を使うだけ
ReactDOM.render(
<React.StrictMode>
<NotifyMessagesProvider>
<App />
</NotifyMessagesProvider>
</React.StrictMode>,
rootElement
);
このコンテクストの使い方の応用で全画面ローディング、エラーハンドリング、ページ遷移時のメッセージなど様々なグローバルなものを定義できます。