はじめに
React初学者の駆け出しエンジニアです。
RailsAPI + Reactのアプリケーションの作成中、「投稿が完了しました。」等のトースト通知機能を実装したいと考え方法を調べたのですが、記事も少ないのか答えが中々見つけられず悪戦苦闘しましたので、ここに記しておきます。
今回はMaterial-uiのSnackbarを使用しました。
またReact側ではhooksを利用しています。キーとなるのは
useState, useContextの2つです。
ちなみにuseContextについては以下の動画シリーズが滅茶苦茶参考になりましたので紹介しておきます。
多分イギリス英語ですかね?個人的にはかなり聞き取りやすかったですし、字幕ありにすれば理解は可能だと思います。
React Context & Hooks Tutorial #1 - Introduction
※あくまで「初学者が自分なりに実装してみた」方法なので、違和感を感じる部分やおかしな方法を取っている部分もあるかと思いますので、ご了承ください。
ファイル構成
src
|--apis //APIと通信するメソッドをまとめたディレクトリ
| |-Tasks.js //Taskに関するAPIメソッド
|--components
| |-Tasks.jsx //Tasks関連の上位コンポーネント。APIメソッド等はここから呼び出す
| |-SimpleSnackbar.jsx //Snackbarコンポーネント
|--contexts
| |-SnackbarContext.jsx //Snackbar用のcontext
|--App.js
なぜuseContext
まずはどのように通知の表示を管理するか?
ざっくりとしたイメージで、useStateで表示、非表示を切り替えれば良さそうです。
const [ snackState, setSnackState ] = useState({
isOpen: false, //boolean型で表示、非表示を切り替える
type: '', //success, infoなどの背景色を変えるもの
message: '', //中に入れるメッセージ内容
})
正直なところ、これだけでも実装はできると思います。
Snackbarを表示させたい各コンポーネントにインポートして、useStateでstate定義して、、、っていう風にやればできるのではないでしょうか。
しかしながら、この記事には書いていませんが私の場合はコンポーネントを細かく複数ファイルに分類しており、その各所でトースト通知を使いたい場面が点在しています。その度にこの記述を繰り返すのには違和感を感じた為、他の方法を探しました。結果たどり着いたのがuseContextです。
useContextを使用することによって、何ができるのか?
stateというのは、コンポーネント毎でそれぞれプライベートに管理されているものであり、親コンポーネントから子コンポーネントにそのstateを受け渡すには、propsのようなバケツリレー方式で渡していく必要があります。
しかし、トースト通知を実装する場合、その場面としては、例えば
- ログイン、ログアウトした時:「ログイン(ログアウト)しました。」
- 投稿に完了した:「投稿しました。」
このように複数の場面で使用することが想定されます。
useContextを使用することで、複数のコンポーネントからでもSnackbarのstateを管理、変更できるようになるため、先程紹介したuseStateだけでの実装よりも無駄の無い構築ができます。
#具体的な実装
API通信メソッド
まずはAPIを叩くためのメソッドです。細かい説明は省きますが、ざっくり言えばフォームで受け取った内容をRailsに送ってDBに保存し、JSONで返してもらっているだけです。エラーが出た場合はそれをthrowしてもらいます。
export const postTask = (params) => {
return axios.post(postsIndexUrl, { //postsIndexUrlにはAPIのrouteを入れてます
title: params.title
})
.then(resp => {
return resp.data
})
.catch((error) => {throw error;})
};
createContext
ここからが本題です。
import React, { useState, createContext } from 'react';
export const SnackbarContext = createContext();
export const SnackbarContextProvider = (props) => {
const [ snackState, setSnackState ] = useState({
isOpen: false,
type: '',
message: '',
})
const toggleSnack = (isOpen, type, message) => {
setSnackState({
isOpen: isOpen,
type: type,
message: message,
})
};
return (
<SnackbarContext.Provider value={{snackState, toggleSnack:toggleSnack}}>
{props.children}
</SnackbarContext.Provider>
)
};
簡単に説明すると、createContext()によってSnackbarContextを定義します。(魔法)
次に、SnackbarContextProviderという名前でコンポーネントを作成します。後で登場します
providerということは提供する、ということですね。そして提供するその内容が以下の内容です。
前述もしていますがuseStateでstateの初期値snackStateとそれを操作するsetSnackStateを宣言します。
このままでも良いのですが、他のコンポーネントからsetSnackStateを呼び出す際に引数を渡すだけで使えるように、toggleSnackという名前で関数を定義し、その内部でsetSnackStateを実行するようにしています。
これによりstateを変更する際はtoggleSnack(true, 'type', 'this is the message')とするだけでOKです。
return以降ですが、SnackbarContext.Provider(ドットがあることに注意)コンポーネントを配置し、valueとして、先程定義したsnackStateとtoggleSnackメソッドを渡しています。
また、{props.children}とすることにより、SnackbarContextProvider(ドット無し)コンポーネントによりラップされた子コンポーネント全てにおいて、このvalueで渡されたsnackStateとtoggleSnackメソッドが使用可能になります。
コンポーネントを配置
import React from 'react';
import {
BrowserRouter as Router,
Switch,
Route,
} from 'react-router-dom';
import { Tasks } from './components/Tasks';
import { SignUp } from './components/SignUp';
import { LogIn } from './components/LogIn';
import { SnackbarContextProvider } from './contexts/SnackbarContext';
import { SimpleSnackbar } from './components/SimpleSnackbar';
import Nabvar from './components/Nabvar';
function App() {
return (
<Router>
<SnackbarContextProvider>
<Nabvar />
<SimpleSnackbar />
<Switch>
<Route path='/signup' component={SignUp}/>
<Route path='/login' component={LogIn}/>
<Route exact path='/' component={Tasks}/>
</Switch>
</SnackbarContextProvider>
</Router>
);
}
export default App;
このような感じで、Snackbarを使うコンポーネントをラップします。
これにより、SignUp、LogIn,Tasks全てのコンポーネントにおいてsnackStateとtoggleSnackメソッドが使えるようになります。
実際に呼び出す
では、各コンポーネントで呼び出していきましょう。(今回はTasks.jsxのみで行います)
SimpleSnackbar.jsx
import React, { useContext } from 'react';
import Snackbar from '@material-ui/core/Snackbar';
import Alert from '@material-ui/lab/Alert';
import { SnackbarContext } from '../contexts/SnackbarContext';
export const SimpleSnackbar = () => {
const { snackState, toggleSnack } = useContext(SnackbarContext); //これで呼び出す
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
toggleSnack(false, snackState.type, '');
};
return (
<div>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={snackState.isOpen}
autoHideDuration={4000}
onClose={handleClose}
>
<Alert onClose={handleClose} severity={snackState.type}>
{snackState.message}
</Alert>
</Snackbar>
</div>
);
}
こんな感じです。
const { snackState, toggleSnack } = useContext(SnackbarContext);
これによって先程定義したsnackStateとtoggleSnackが使用できます。
クローズ時に背景色を扱うtypeをなぜ空('')にしないかというと、空にするとトーストが閉じる時に背景色が一瞬白色になって消えるという不自然な感じになってしまうので、敢えて直前のtypeを維持したまま閉じる方法にしました。
ここまでで開いているトーストを閉じる動作は実装できました。
後はタスクの投稿完了時に「投稿しました。(背景緑)」と表示し、エラーが発生した際は「投稿に失敗しました。(背景赤)」と表示させましょう。
Tasks.jsx
必要な部分だけ記述しています
import { SnackbarContext } from '../contexts/SnackbarContext';
import { postTask } from "../apis/Tasks";
export const Tasks = () => {
const { toggleSnack } = useContext(SnackbarContext);
const handleSubmit = () => {
postTask({ //API通信メソッド
//~~~省略~~~//
}).then(data => {
//~~~~省略~~~~//
toggleSnack(true, 'success', 'You created a new Task!') //投稿に成功した時
})
.catch((e) => {
console.error(e)
toggleSnack(true, 'error', 'Create failed!') //失敗した時
})
//~~~省略~~~//
};
};
こんな感じで自由に文字や背景色を変えて使用できます。
ログイン時など他のコンポーネントでも使いたい場合は同じ要領で行えば問題ありません。
以上が私なりに実装してみた例です。
初学者の方の参考になれば幸いです。