コンテクスト – 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 );
このコンテクストの使い方の応用で全画面ローディング、エラーハンドリング、ページ遷移時のメッセージなど様々なグローバルなものを定義できます。