Material-UIのSnackbarを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Material-UIのSnackbarを使用して、メッセージを表示します。Snackbarは、画面の端から出てきて1行程度のお知らせを表示するエリアのことです。 未ログイン時の更新エラー(NOT_AUTHENTICATED_ON_TODO_ACTION)と通信エラー(ADD_TODO_ERRORTOGGLE_TODO_ERROR)の時にSnackbarにエラーメッセージを表示します。データの送信中と送信完了メッセージについては、メッセージの表示をやめます。(次ページでアイコンによる表示を行います。)

f:id:yucatio:20181214224609p:plain

Noticeコンポーネントの移動

Noticeコンポーネントをtodosディレクトリから1つ上のcomponentsディレクトリに移動し、todosに限らないお知らせの表示の役割を持たせます。表示する場所もAppコンポーネントに移動します。

src/components/todos/Notice.jssrc/components/Notice.jsにリネームします。

src/components/Notice.js

const Notice = ({ notice }) => {  // 変更
  // 略
}

Notice.propTypes = {  // 変更
  notice: PropTypes.string
}

export default connect(  // 変更
  mapStateToProps
)(Notice)  // 変更

src/components/todos/index.js

// import Notice from './Notice' を削除

class TodoComponent extends React.Component {
  // 略
  render() {
    const {isOwnTodos, match: { params: {uid}}, classes} = this.props;
    return (
      <div className={classes.root}>
        <div className={classes.todoListRoot}>
          <Paper className={classes.todoListContent}>
            <Title isOwnTodos={isOwnTodos} uid={uid} />
            {isOwnTodos && <AddTodo uid={uid} />}
            {/* <Notice /> を削除 */}
            <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} />
          </Paper>
        </div>
        <FilterNav />
      </div>
    )
  }
}

src/components/App.js

import Notice from './Notice'  // 追加

const App = ({classes}) => (
  <BrowserRouter>
    <div>
      <CssBaseline />
      <Header />
      <div className={classes.toolbar} />
      <Switch>
        <Route exact path="/" component={Dashboard} />
        <Route exact path="/users/:uid/todos" component={TodoComponent} />
        <Route component={NoMatch} />
      </Switch>
      <Notice />  {/* 追加 */}
    </div>
  </BrowserRouter>
)

Actionの作成

Snackbarが閉じられる時に呼ばれるactionを定義します。

src/actions/index.js

// notice
export const CLOSE_NOTICE = 'CLOSE_NOTICE'  // 追加

src/actions/noticeActions.js(新規作成)

import {CLOSE_NOTICE} from './'

export const closeNotice = () => ({
  type: CLOSE_NOTICE
})

Reducerの作成

新規にnotice reducerを作成します。todo reducerは削除します。

src/reducers/notice.js(新規作成)

import { NOT_AUTHENTICATED_ON_TODO_ACTION, ADD_TODO_ERROR, TOGGLE_TODO_ERROR,
  LOCATION_CHANGE_ON_TODOS, LOGOUT_SUCCESS, CLOSE_NOTICE}
   from '../actions/'

const INITIAL_STATE = { text: '', level: 'info', open: false }  // #1

const notice = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case NOT_AUTHENTICATED_ON_TODO_ACTION :
      return { text: 'タスクを追加・変更するにはログインしてください',
          level: 'warning', open: true }  // #2
    case ADD_TODO_ERROR :
    case TOGGLE_TODO_ERROR :
      return { text: 'エラーが発生しました。時間をおいて再度お試しください。',
        level: 'error', open: true }  // #3
    case CLOSE_NOTICE :
      return { ...state, open: false }  // #4
    case LOCATION_CHANGE_ON_TODOS :
    case LOGOUT_SUCCESS :
        return { ...state, open: false }  // #5
    default:
      return state
  }
}

export default notice
  • noticeのstateはtextとlevel、openの3つのプロパティを持ちます。textはSnackbarに表示する文字列、levelは{‘success', 'warning', 'error', 'info’}のいずれかで、Snackbarに表示されるアイコンと背景色をコントロールします。openはSnackbarが表示されているかどうかを示す真偽値です。初期値はそれぞれ空文字、 'info’、falseです(#1)。
  • 未ログイン時にタスクの追加・更新を行った場合はwarningレベルのSnackbarにします。openをtrueにしてSnackbarを表示します(#2)。
  • タスクの追加・更新時のエラーの時はerrorレベルにしてSnackbarを表示します(#3)。
  • CLOSE_NOTICE actionの時にopenをfalseにしてSnackbarを閉じます(#4)。
  • ページ遷移の際やログアウトの際にはSnackbarを閉じます(#5)。

src/reducers/todos.jsを削除します。

src/reducers/index.js

// import todos from './todos'  を削除
import notice from './notice' // 追加

export default combineReducers({
  firebase: firebaseReducer,
  auth,
  // todos, を削除
  notice,  // 追加
  visibilityFilter
})

レベルごとに表示を出し分ける

レベル('success', 'warning', 'error', 'info’の4つ)ごとにSnackbarの色を変えます。 公式ページのCustomized Snackbars を参考に実装を進めます。

先にSnackbarContentのラッパコンポーネントを作成します。

src/components/util/snackbar/LevelSnackbarContentWrapper.js(新規作成)

import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { withStyles } from '@material-ui/core/styles'
import IconButton from '@material-ui/core/IconButton'
import SnackbarContent from '@material-ui/core/SnackbarContent'
import CheckCircleIcon from '@material-ui/icons/CheckCircle'
import ErrorIcon from '@material-ui/icons/Error'
import InfoIcon from '@material-ui/icons/Info'
import CloseIcon from '@material-ui/icons/Close'
import WarningIcon from '@material-ui/icons/Warning'
import green from '@material-ui/core/colors/green'
import amber from '@material-ui/core/colors/amber'

const variantIcon = {
  success: CheckCircleIcon,
  warning: WarningIcon,
  error: ErrorIcon,
  info: InfoIcon,
}

const styles = theme => ({
  success: {
    backgroundColor: green[600],
  },
  error: {
    backgroundColor: theme.palette.error.dark,
  },
  info: {
    backgroundColor: theme.palette.primary.dark,
  },
  warning: {
    backgroundColor: amber[700],
  },
  icon: {
    fontSize: 20,
  },
  iconVariant: {
    opacity: 0.9,
    marginRight: theme.spacing.unit,
  },
  message: {
    display: 'flex',
    alignItems: 'center',
  },
})

const LevelSnackbarContent = ({ classes, className, message, onClose, variant, ...other }) => {
  const Icon = variantIcon[variant]
  return (
    <SnackbarContent
      className={classNames(classes[variant], className)}
      aria-describedby="client-snackbar"
      message={
        <span id="client-snackbar" className={classes.message}>
          <Icon className={classNames(classes.icon, classes.iconVariant)} />
          {message}
        </span>
      }
      action={[
        <IconButton
          key="close"
          aria-label="Close"
          color="inherit"
          className={classes.close}
          onClick={onClose}
        >
          <CloseIcon className={classes.icon} />
        </IconButton>,
      ]}
      {...other}
    />
  )
}

LevelSnackbarContent.propTypes = {
  classes: PropTypes.object.isRequired,
  className: PropTypes.string,
  message: PropTypes.node,
  onClose: PropTypes.func,
  variant: PropTypes.oneOf(['success', 'warning', 'error', 'info']).isRequired
}

export default withStyles(styles)(LevelSnackbarContent)

最後に、お知らせ(Notice)の表示部分を実装します。

src/components/Notice.js

import React from 'react'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import Snackbar from '@material-ui/core/Snackbar';
import { closeNotice } from '../actions/noticeActions'
import LevelSnackbarContentWrapper from './util/snackbar/LevelSnackbarContentWrapper'

const Notice = ({ text, level, open, handleClose }) => (
  <Snackbar  // #1
    anchorOrigin={{
      vertical: 'bottom',
      horizontal: 'center',
    }}
    open={open}
    autoHideDuration={30000}
    onClose={handleClose}
  >
    <LevelSnackbarContentWrapper  // #2
        onClose={handleClose}
        variant={level}
        message={text}
      />
  </Snackbar>
)
        
Notice.propTypes = {
  text: PropTypes.string.isRequired,
  level: PropTypes.oneOf(['success', 'warning', 'error', 'info']).isRequired,
  open: PropTypes.bool.isRequired,
  handleClose: PropTypes.func.isRequired
}

const mapStateToProps = state => {
  return {
    text: state.notice.text,
    level: state.notice.level,
    open: state.notice.open,
  }
}

const mapDispatchToProps = (dispatch, {uid}) => ({
  handleClose: () => {
    dispatch(closeNotice())
  }
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Notice)
  • Snackbarの設定をします(#1)。anchorOriginに{vertical: 'bottom', horizontal: 'center’}を指定して、中央下にSnackbarを表示します。表示状態(open)はstateか取得します。autoHideDurationはSnackbarの表示時間(ミリ秒)です。今回は30000を指定して30秒経過したら自動的にSnackbarが隠れるようにしました。onCloseではcloseNotice actionがdispatchされるようにしています。
  • LevelSnackbarContentWrapperの設定をします(#2)。variantにlevelを設定します。messageにtextを渡します。

実行結果

実行結果です。通常の操作ではエラーが発生しないので、少しソースを変更してエラーを出しています。

f:id:yucatio:20181214224609p:plain

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com