yucatio@システムエンジニア

趣味で作ったものいろいろ

タスクの送信状態の表示(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

タスクの送信状態を表示するように変更します。タスクの各行に送信状態を表すアイコンを表示します。

今回のタスク管理アプリの場合、送信状態を表示することは必須ではありませんが、 チャットアプリのような相手がいて即時性が求められるアプリの場合は、サーバへの送信状況を表示した方がよいでしょう。

f:id:yucatio:20181215222506p:plain

方針

タスクの送信状態を、各タスクの横にアイコンを表示することで示します。

  • サーバにデータを送信中は送信中のマークをタスクの横に表示します。今回は右上向きの矢印を表示します。
  • サーバへのデータ送信が終了後、アイコンを消します。
  • エラーが発生した場合はエクスクラメーションマークのアイコンを表示します。

stateオブジェクトの設計

各タスクの状態を持つtodoStatusesオブジェクトを用意します。todoStatusesの各プロパティはキーをtodoIdにすればよさそうです。以下の"aaabbbcccxxxfff""cccdddeeefff""gggeeefffttt"はtodoIdです。

state = {
  todoStatuses : {
      "aaabbbcccxxxfff" : {
        status : “sending},
     "cccdddeeefff" : {
        status : “success},
     "gggeeefffttt" : {
        status : “error}
  }
}

新規登録時のtodoIdの事前取得

上記のデータ構造を採用する際、 タスクの更新時にはすでにtodoIdが分かっているのでtodoIdをキーとすることは可能です。しかし新規登録では、現在の実装ではtodoIdが分かるのはタスクの登録後です。それでは送信中のステータスを表示することができません。

そこで、新規タスクを登録する前にtodoIdを取得するようにコードを変更します。また、reducerにtodoIdを渡すようにも変更します。

src/actions/todoActions.js

const addTodoRequest = (todoId) => ({  // todoIdを追加
  type: ADD_TODO_REQUEST,
  todoId,  // 追加
})

const addTodoSuccess = (todoId) => ({  // todoIdを追加
  type: ADD_TODO_SUCCESS,
  todoId,  // 追加
})

const addTodoError = (todoId, err) => ({  // todoIdを追加
  type: ADD_TODO_ERROR,
  todoId,  // 追加
  err,
})

export const addTodo = (uid, text) => {
  return (dispatch, getState, {getFirebase}) => {
    if (!uid) {
      dispatch(notAuthenticatedOnTodoAction());
      return;
    }
    // dispatch(addTodoRequest()); は下に移動
    const firebase = getFirebase();
    const id = firebase.push(`todos/${uid}`).key;  // #1
    dispatch(addTodoRequest(id));  // idを引数に追加
    const createdAt = moment().valueOf();
    firebase.set(`todos/${uid}/${id}`,{  // #2
      completed: false,
      text,
      _createdAt : createdAt,
      _updatedAt : createdAt
    })
    .then(() => {
      dispatch(addTodoSuccess(id));  // idを引数に追加
    }).catch(err => {
      dispatch(addTodoError(id, err));  // idを引数に追加
    });
  }
}
  • firebase.push(`todos/${uid}`).keyで新しいデータのkey(id)を取得することができます(#1)。この操作はサーバ通信を行わないため、すぐに結果が返ってきます。
  • 上で取得したidを使ってtodos/${uid}/${id}にデータをセットします(#2)。pushからsetに変更してあることに注意してくだい。

更新時にtodoIdをactionに渡す

更新時、現在の実装ではタスク名と完了グラグを渡していますが、todoIdを渡すように変更します。

src/actions/todoActions.js

const toggleTodoRequest = (todoId) => ({  // text, completedを削除し、todoIdを追加
  type: TOGGLE_TODO_REQUEST,
  // text, を削除
  // completed を削除
  todoId,
})

const toggleTodoSuccess = (todoId) => ({  // text, completedを削除し、todoIdを追加
  type: TOGGLE_TODO_SUCCESS,
  // text, を削除
  // completed を削除
  todoId,
})

const toggleTodoError = (todoId, err) => ({  // text, completedを削除し、todoIdを追加
  type: TOGGLE_TODO_ERROR,
  // text, を削除
  // completed, を削除
  todoId,
  err,
})

export const toggleTodo = (uid, id) => {
  return (dispatch, getState, {getFirebase}) => {
    if (!uid) {
      dispatch(notAuthenticatedOnTodoAction());
      return;
    }
    const firebase = getFirebase();
    const state = getState();
    const todo = state.firebase.data.todos[uid][id];
    dispatch(toggleTodoRequest(id));  // todo.text, !todo.completedを削除し、idを追加
    const updatedAt = moment().valueOf();
    firebase.update(`todos/${uid}/${id}`, {
      completed: ! todo.completed,
      _updatedAt : updatedAt
    })
    .then(() => {
      dispatch(toggleTodoSuccess(id));  // todo.text, !todo.completedを削除し、idを追加
    }).catch(err => {
      dispatch(toggleTodoError(id, err));  // todo.text, !todo.completedを削除し、idを追加
    });
  }
}

reducerの実装

actionを受け取った時に、todoIdごとにステータスを設定します。

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

import { LOCATION_CHANGE_ON_TODOS, LOGOUT_SUCCESS,
  ADD_TODO_REQUEST, ADD_TODO_SUCCESS, ADD_TODO_ERROR,
  TOGGLE_TODO_REQUEST, TOGGLE_TODO_SUCCESS, TOGGLE_TODO_ERROR,
   }
   from '../actions/'

const INITIAL_STATE = {}

const todoStatuses = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case ADD_TODO_REQUEST:
      return { ...state, [action.todoId]: {status : 'sending'}}  // #1
    case ADD_TODO_SUCCESS:
      return { ...state, [action.todoId]: {status : 'success'}}
    case ADD_TODO_ERROR:
      return { ...state, [action.todoId]: {status : 'error'}}
    case TOGGLE_TODO_REQUEST:
      return { ...state, [action.todoId]: {status : 'sending'}}
    case TOGGLE_TODO_SUCCESS:
      return { ...state, [action.todoId]: {status : 'success'}}
    case TOGGLE_TODO_ERROR :
      return { ...state, [action.todoId]: {status : 'error'}}
    case LOCATION_CHANGE_ON_TODOS:
    case LOGOUT_SUCCESS :
      return INITIAL_STATE
    default:
      return state
  }
}

export default todoStatuses
  • stateに、todoIdをキーとしstatusプロパティを持ったオブジェクトを追加(または上書き)します(#1)。[action.todoId]action.todoIdが表す値がキーになります。

src/reducers/index.js

import todoStatuses from './todoStatuses'  // 追加

export default combineReducers({
  firebase: firebaseReducer,
  auth,
  notice,
  todoStatuses,  // 追加
  visibilityFilter
})

送信ステータスの表示

送信ステータスを表示します。

todoStatusをstateから読み込みます。

src/containers/todos/VisibleTodoList.js

const mapStateToProps = ({todoStatuses, visibilityFilter, firebase: {auth, data : {todos, users}}}, {uid}) => { // todoStatusesの追加
  return {
    todos: getVisibleTodos(todos && todos[uid], visibilityFilter),
    todoStatuses,  // 追加
  }
}

todoStatuses[key]を各todoに渡します。

src/components/todos/TodoList.js

const TodoList = ({todos, isOwnTodos, todoStatuses, onTodoClick, classes}) => {  // todoStatusesを追加
  // 略
  return (
    <List>
      {Object.keys(todos).map(
        (key) => (
          <Todo
            key={key}
            isOwnTodos={isOwnTodos}
            {...todos[key]}
            todoStatus={todoStatuses[key]}  // 追加
            onClick={isOwnTodos ? (() => onTodoClick(key)) : (() => {})} />
        )
      )}
    </List>
  )
}

TodoList.propTypes = {
  // 略
  todoStatuses: PropTypes.object.isRequired,  // 追加
}

送信ステータスによってアイコンを出し分けます。

src/components/todos/Todo.js

import Tooltip from '@material-ui/core/Tooltip'  // 追加
import CallMade from '@material-ui/icons/CallMade'  // 追加
import Error from '@material-ui/icons/Error'  // 追加

// StatusIcon関数の追加
const StatusIcon = (todoStatus) => {
  if (!todoStatus) {
    return null;
  }

  if (todoStatus.status === 'sending') {
    return (
      <Tooltip title="送信中">
        <CallMade />
      </Tooltip>
    )
  }
  if (todoStatus.status === 'error') {
    return (
      <Tooltip title="エラー">
        <Error />
      </Tooltip>
    )
  }
  return null
}

const Todo = ({isOwnTodos, onClick, completed, text, todoStatus}) => (  // todoStatusの追加
  <ListItem
    button={isOwnTodos}
    onClick={onClick}
  >
    {CheckIcon(isOwnTodos, completed)}
    <ListItemText inset>
      <span style={ {textDecoration: completed ? 'line-through' : 'none'}}>{text}</span>
    </ListItemText>
    {StatusIcon(todoStatus)}  {/* 追加 */}
   </ListItem>
)

Todo.propTypes = {
  // 略
  todoStatus: PropTypes.shape({  // 追加
    status: PropTypes.oneOf(['sending', 'success', 'error']).isRequired
  })
}

実行結果

実行結果です。

送信中は右上向きの矢印とマウスオーバー時に送信中のツールチッブが表示されます。

f:id:yucatio:20181215222506p:plain

エラー時はエクスクラメーションマークとマウスオーバー時にエラーのツールチッブが表示されます。 (通常の操作ではエラーが発生しないので、少しソースを変更してエラーを出しています。)

f:id:yucatio:20181215222521p:plain

以上でタスクの送信状態を表示することができました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com