react-router v4使用時、ページ遷移時にactionをdispatchする (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

前回までで他のユーザのタスクは閲覧できて、自身のタスクは閲覧・編集できるようになりました。しかし、まだバグが2つ残っているので、そのうち1つを修正します。

バグ再現手順

  1. ログインします
  2. ログインユーザ以外のタスクを表示します。
  3. “タスクを編集する”を押して、ログインユーザのタスクを表示します。
  4. タスクをクリックして、”xxxのステータスを(未)完了に変更しました”と表示します。
  5. ブラウザバックします。
  6. ログインユーザ以外のタスク”xxxのステータスを(未)完了に変更しました”と表示されます。

f:id:yucatio:20181021145851p:plain

修正方針

ざっと2つの修正方法が思いつきます。

  1. ログインユーザ以外のタスク一覧を表示している時は、NoticeForTodoコンポーネントを表示しない
  2. 画面遷移ごとにstate.todos.noticeをクリアする

1番目の方法は実装しやすいですが、いくつかの画面遷移後にまたログインユーザのタスク一覧画面に戻ってきた時に、前回の更新メッセージが表示されてしまいます。それでは根本的な解決とは言えなさそうです。また、NoticeForTodoコンポーネントとstate.todos.noticeはログインユーザのタスクに関係なく使用したいので、今回は、2番目の方法を採用します。

ついでに、画面遷移時に、VisibilityFilterも初期化します。

実装方針

実装方法を紹介する前に、react-routerのバージョン3から4への変更点について触れておきます。

react-router v3のonEnter, onChange, onLeave

react-routerのバージョン3には、RouteコンポーネントにonEnterとonChange, onLeaveというpropsを渡すと、component propsに渡したコンポーネントがそれぞれマウントされる時、パスが変更された時、アンマウントされる時に呼ばれる関数を指定することができました。しかしバージョン4で廃止されています。

react-router v4での実装方法

react-router v4では、ページの遷移をcomponentWillMount/componentDidMount、componentWillReceiveProps、componentWillUnmountで検知します。

同一コンポーネント内でのURLの変更を検知するのには、公式のドキュメント(react-router/history)に書いてある通り、componentWillReceivePropsを使います。

  componentWillReceiveProps(nextProps) {
    const locationChanged = nextProps.location !== this.props.location;
  }

実装

TodoComponentがマウントされた時とURLが変更された時に、locationChangeOnTodos actionをdispatchします。

先にactionとreducerを実装します。

src/action/index.js

// todo actions
export const LOCATION_CHANGE_ON_TODOS = 'LOCATION_CHANGE_ON_TODOS'  // 追加

src/action/todoAction.js

import { LOCATION_CHANGE_ON_TODOS, //他のimportは省略
 }
   from './'

export const locationChangeOnTodos = () => ({  // 追加
  type: LOCATION_CHANGE_ON_TODOS
})

src/reducers/todos.js

import { LOCATION_CHANGE_ON_TODOS, //他のimportは省略
 }
   from '../actions/'

// 中略

const todos = (state = {}, action) => {
  switch (action.type) {
    // 中略
    case LOCATION_CHANGE_ON_TODOS:  // 追加
    case LOGOUT_SUCCESS :
      return {}
    default:
      return state
  }
}

src/reducers/visibilityFilter.js

import { LOCATION_CHANGE_ON_TODOS,  // 他のimportは省略
 } from '../actions/'
// 中略

const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter
    case LOCATION_CHANGE_ON_TODOS:  // 追加
    case LOGOUT_SUCCESS:
      return VisibilityFilters.SHOW_ALL
    default:
      return state
  }
}

Viewを実装します。マウントされる時とURLの、/users/:id/todos:idが変更された時に、locationChangeOnTodos actionをdispatchします。

src/components/TodoComponent.js

// 前略
import { locationChangeOnTodos } from '../actions/todoActions'  // #1

class TodoComponent extends React.Component {  // #2
  componentWillMount() {  // #3
    this.props.locationChange()
  }

  componentWillReceiveProps(nextProps) {  // #4
    if(nextProps.location !== this.props.location) {  // #5
      this.props.locationChange()
    }
  }

  render() {  // #6
    const {isOwnTodos, match: { params: {uid}}} = this.props;
    return (
      <div>
        {isOwnTodos && <AddTodo uid={uid} />}
        <NoticeForTodo />
        <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} />
        <Footer />
      </div>
   )
  }
}

// 中略

TodoComponent.propTypes = {
  // 中略
  locationChange: PropTypes.func.isRequired  // #7
}

// 中略

const mapDispatchToProps = (dispatch) => ({
  locationChange: () => dispatch(locationChangeOnTodos())  // #8
})

TodoComponent = connect(
  mapStateToProps,
  mapDispatchToProps  // #9
)(TodoComponent)
  • locationChangeOnTodosをimportします(#1)。
  • TodoComponentを、関数コンポーネント(function component)からReact.Componentを拡張したクラスに変更します(#2)。
  • componentWillMountlocationChangeを呼び出します(#3)。呼び出すことで、locationChangeOnTodos actionがdispatchされます。
  • componentWillReceivePropsを定義します(#4)。この関数は、propsが変更される時に呼び出されます。
  • componentWillReceivePropsでは、前後のprops.locationの値を比較します(#5)。URLが変更されていればtrueになります。その場合にlocationChangeを呼び出します。
  • 関数コンポーネントからクラスに変更になったため、renderメソッドを記述します(#6)。propsの受け取り方も変更されています。
  • locationChangeに対応するPropTypeを追加します(#7)。
  • locationChangeOnTodosをdispatchする関数を、locationChangeという名前で下位コンポーネントに渡します(#8)。
  • mapDispatchToPropsconnectの引数に追加します(#9)。

動作確認

  1. ログインします
  2. ログインユーザ以外のタスクを表示します。
  3. “タスクを編集する”を押して、ログインユーザのタスクを表示します。
  4. タスクをクリックして、”xxxのステータスを(未)完了に変更しました”と表示します。
  5. ブラウザバックします。
  6. ログインユーザ以外のタスク”xxxのステータスを(未)完了に変更しました”と表示されます。

f:id:yucatio:20181021145907p:plain

Noticeが消えて、うまく動きました。

visibilityFilterの動作確認をします。

f:id:yucatio:20181021150710p:plain

ページ遷移時にfilterを全て表示(SHOW_ALL)にできました。

以上で、ページ遷移時にactionをdispatchして、noticeとvisibilityFilterを変更することができました。

更新メッセージの表示と消去について

シングルページアプリケーションの場合、リンクをクリックしてページ遷移しても、状態は初期化されません。ページの再読み込みが必要なくなるのでページの表示が高速になる一方、状態も更新されないので、きちんと値を管理しないと意図しない表示になります。

今回のアプリケーションでは、更新完了時に”完了しました”とメッセージを表示しました。 開発中は、”メッセージが表示されること”に気が向かってしまい、”メッセージが消えること”まで気を使えないことが多くあります。

更新のメッセージについては、一定時間のみ画面に表示し、自動で消えるという実装をすると、別画面で更新メッセージが表示されることを防げます。設計段階でそのような挙動を盛り込むことを検討するとよいと思います。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com