「保存に成功しました」だったり「クーポンの適応に失敗しました」だったり
ユーザーに何かしらの内容を一時的に伝えたい時があると思います。
そのような時に用いられる通知のUIの一種であるトーストを実装していきます。
イメージとしてはこのようなものです
トーストの要件
下記の要件を満たすトーストを作っていきます。
- 1つずつ順番に表示される
- 表示されてちょっと経ったら自動で消える
- どのコンポーネントからでも新しいトーストを追加できる
- デザインはがんばらない
実装
1. トーストの情報を保持する場所を用意する
どこからでも追加と参照ができるように、ReduxのStoreに持たせます。
Store自体の設定については割愛します。
// toast.ts
import { Reducer, AnyAction } from 'redux'
import { isType, actionCreatorFactory } from 'typescript-fsa'
import { useSelector } from 'react-redux'
export type Toast = {
message: string
}
type Toasts = Toast[]
// action
const actionCreator = actionCreatorFactory('TOAST')
export const Push = actionCreator<{ toast: Toast }>('PUSH')
export const Shift = actionCreator('SHIFT')
// reducer
const initialState: Toasts = []
export const reducer: Reducer<Toasts> = (
state: Toasts = initialState,
action: AnyAction,
) => {
if (isType(action, Push)) {
const { toast } = action.payload
return state.concat([toast])
}
if (isType(action, Shift)) {
return state.slice(1)
}
return state
}
export default reducer
// selector
export const GetToasts = (): Toasts => useSelector(
(state: { toasts: Toasts }) => state.toasts,
)
Stateをファイルの中で定義していますが
プロジェクトに合わせて読み込むと良いと思います。
2. トーストを表示するコンポーネントを用意する
このコンポーネントでは表示の状態を管理するためにuseState
、
トーストの変化を監視するためにuseEffect
といったReact Hooksの機能を用います。
トーストの情報はStoreに保存し、selectorも用意してあるので
どこからでも簡単に呼び出せます。
// ToastContainer.tsx
import React, { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { GetToasts, Shift } from 'toast.ts'
import style from 'style.scss'
const timeout = async (ms: number) => (
new Promise((resolve: () => void): void => {
setTimeout(() => resolve(), ms)
})
)
const ToastContainer: React.FC = () => {
const toasts = GetToasts()
const [visible, setVisible] = useState(false)
const dispatch = useDispatch()
useEffect(() => {
if (toasts.length === 0 || visible) {
return
}
const showToast = async () => {
setVisible(true)
await timeout(3000)
setVisible(false)
await timeout(500)
dispatch(Shift())
}
showToast()
}, [toasts])
return (
<div className={style.toastContainer}>
<div className={`${style.toast} ${visible && style.visible}`}>
{toasts.length > 0 && toasts[0].message}
</div>
</div>
)
}
export default ToastContainer
トーストの表示の流れはこのようになります
-
toasts
が増えたら監視しているuseEffect
が発火 - トーストが見えるように
visible
をtrue
に変更 - ちょっと経ったら
visible
をfalse
に変更して隠す -
dispatch(Shift())
で先頭のtoasts
を削除 -
toasts
が更新されるのでuseEffect
がもう一度発火 - まだ
toasts
があったら2に戻る
あとは適当にcssを当ててあげればOKです
// style.scss
.toast {
width: 320px;
padding: 16px 0;
color: #fff;
text-align: center;
background-color: red;
opacity: 0;
transition: opacity .6s;
&.show {
opacity: 1;
transition: opacity .2s;
}
}
3. トーストを追加する
Reduxでトーストを管理しているので、どこで追加しても大丈夫です。
例えば何かしらのAPIを呼んだときはこのようにトーストを追加します
save()
.then(() => dispatch(Push({ toast: { message: '保存しました' } })))
.catch(() => dispatch(Push({ toast: { message: '保存に失敗しました' } })))
トーストの内容に応じて色を変えたりしたい場合は、type
なども持たせましょう。
まとめ
バリデーションや表示の仕方に関しては実装したい仕様に合わせて適宜変更して下さい。
React Hooksを使うと煩雑だった処理がすっきり書けるので楽しいです。
参考
- Redux: https://redux.js.org
- React Hooks: https://ja.reactjs.org/docs/hooks-intro.html