yucatio@システムエンジニア

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

Cloud Functions for Firebaseの導入とfirebase init functionsの実行 (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

アプリにCloud Functions for Firebaseを導入します。

Cloud Functions for Firebaseでできること

Cloud Functions for Firebaseでは、ユーザがログインしたり、データベースに書き込まれた際に関数を実行できます。関数内では、以下の処理などを行うことができます。

  • DBへの書き込み
  • ユーザへのプッシュ通知
  • 外部API連携

今回は、タスクが作成・更新された時に、DBに書き込みを行います。

firebase init functionsの実行

Cloud Functionsを使用するために、firebase init functionsを実行して、Cloud Functionsに必要なファイルを作成します。

# プロジェクトのルートディレクトリに移動
$ sh todo-sample

$ firebase init functions

firebase initの時と同様、また燃えているFirebaseの文字が表示されます。

f:id:yucatio:20181021232602p:plain

“既存のFirebaseプロジェクトディレクトリを初期化しようとしていることに気をつけてください。”とのメッセージが表示されます。

f:id:yucatio:20181021232647p:plain

Project Setupはすでに完了しているのでスキップされます。

f:id:yucatio:20181021232726p:plain

Function Setupでは、使用する言語が選べます。このチュートリアルでは、JavaScriptを使用します。

f:id:yucatio:20181021232824p:plain

続いて、ESLintを使用するかどうか聞かれますので、”y”を選択します。

f:id:yucatio:20181021232850p:plain

npmで依存するパッケージを自動でインストールするか聞かれますので、”Y”を入力します。

f:id:yucatio:20181021232920p:plain

依存パッケージがインストールされ、設定が完了しました。

f:id:yucatio:20181021233507p:plain

ディレクトリ構成は以下のようになります。

todo-sample/
|_ functions/      # 新規ディレクトリ
   |_ .eslintrc.json
   |_ index.js
   |_ node_modules/
   |  |_ ...modules...
   |_ package-lock.json
   |_ package.json

firebase.jsonに以下の設定が追加されています。

{
  // 略
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint"
    ]
  }
}

以上でCloud Functions for Firebaseの開発を行う準備ができました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

react-redux-firebase使用時、ログアウト時にデータが消えないようにする (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

他のユーザのタスクは閲覧できて、自身のタスクは閲覧・編集できるようになったアプリに発生した2つのバグのうち、もう1つのバグを修正します。

バグ再現手順

  1. ログインします
  2. 自身のタスク一覧が表示されることを確認します。
  3. ログアウトします。
  4. “タスク一覧を読み込み中…” がずっと表示され、タスク一覧が表示されません。

f:id:yucatio:20181021153110p:plain

バグの原因

react-redux-firebaseではログアウト時にstate.firebase.datastate.firebase.authの内容を全てクリアします。そのため、todosの内容がundefinedになり、"読み込み中"がずっと表示されます。

修正方針

react-redux-firebaseに preserveOnLogout を設定します。 この設定をすると、ログアウト時にもデータが消えません。

コーディング

preserveOnLogoutを、reactReduxFirebaseに設定します。todosの内容とusersの内容をログアウト時にも表示したいので、この2つを設定します。

src/index.js

const createStoreWithFirebase = compose(
  applyMiddleware(thunk.withExtraArgument({getFirebase}),
  reactReduxFirebase(firebase, {userProfile: 'users', preserveOnLogout: ['todos', 'users']})  // 変更
)(createStore);

動作確認

  1. ログインします
  2. タスク一覧が表示されることを確認します。
  3. ログアウトします。
  4. タスク一覧が引き続き表示されることを確認します。

f:id:yucatio:20181021153526p:plain

以上でログアウト時に”読み込み中”がずっと表示されるバグが修正できました。 こちらのバグも見つけにくいものでした。ログアウト時のテストもテストケースに含めておくとよいですね。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

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

ログインしたユーザ自身のタスク以外は変更できないように修正する (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

ログインしたユーザ自身のタスク以外は変更できないように修正します。

ログインしたユーザのタスク一覧以外を表示している時は、以下のようにします。

  • タスク追加フォーム(AddTodoコンポーネント)を非表示にする
  • タスクをクリックされた時に、タスク完了フラグの切り替えをしない

ログインしたユーザ自身のタスク一覧でないときは、タスク追加フォーム(AddTodoコンポーネント)を非表示にする

自身のタスク一覧でないときは、タスク追加フォーム(AddTodoコンポーネント)を非表示にします。

パスのuidとログインしているユーザののuidを比較して、違っている場合はAddTodoコンポーネントを非表示にします。 パスのuidとログインしているユーザののuidが同一かどうかはisOwnTodosという変数で管理します。

src/component/TodoComponent.js

let TodoComponent = ({isOwnTodos, match: { params: {uid}}}) => (  // #1
  <div>
    {isOwnTodos && <AddTodo uid={uid} />}  {/* #2 */}
    <NoticeForTodo />
    <VisibleTodoList uid={uid} />
    <Footer />
  </div>
)

TodoComponent.propTypes = {
  isOwnTodos: PropTypes.bool.isRequired,  // #3
  // 後略
}

const mapStateToProps = ({firebase: {auth}}, {match}) => ({
  isOwnTodos: auth.uid === match.params.uid,  // #4
})
  • isOwnTodosをpropsから受け取ります(#1)。
  • isOwnTodosがtrueの時のみAddTodoコンポーネントを描画します(#2)。
  • isOwnTodosのPropTypeを追加します(#3)。
  • ログインしているユーザのuid(state.firebase.auth.uid)と、パスで指定されたuid(this.props.match.params.uid)を比較して、同一ならば、isOwnTodosの値をtrueにします(#4)。

ログインしたユーザ自身のタスク一覧でないときは、タスク完了フラグの切り替えをしない

上記と同様に、自身のタスク一覧でないときは、タスク完了フラグの切り替えをしないように処理を書き換えます。

src/component/TodoComponent.js

let TodoComponent = ({isOwnTodos, match: { params: {uid}}}) => (
  <div>
    {isOwnTodos && <AddTodo />}
    <NoticeForTodo />
    <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} />  {/* #1 */}
    <Footer />
  </div>
)
  • VisibleTodoListisOwnTodosを渡します(#1)。このpropsは、VisibleTodoListを通して、TodoListに渡されます。

src/component/TodoList.js(#1, #2などのコメントは削除してください)

const TodoList = ({displayName, todos, isOwnTodos, onTodoClick}) => {  // #1
  // 中略
  return (
    <div>
      {displayName && <div>{displayName} さんのタスク一覧</div>}
      <ul>
        {Object.keys(todos).map(
          (key) => (
            <Todo
              key={key}
              {...todos[key]}
              onClick={isOwnTodos ? (() => onTodoClick(key)) : (() => {})} />  {/* #2 */}
          )
        )}
      </ul>
    </div>
  )
}

Todo.propTypes = {
  // 中略
  isOwnTodos: PropTypes.bool.isRequired,  // #3
  // 後略
}
  • isOwnTodosをpropsから受け取ります(#1)。
  • isOwnTodosがtrueの時のみ、onTodoClickが実行されるように変更します(#2)。コードが読みにくいですが、3項演算子を使用して、isOwnTodosがtrueの時() => onTodoClick(key)を返し、falseの時() => {}を返します。
  • isOwnTodosのPropTypeを追加します(#3)。

おまけ: 自身のタスクの時、ユーザ名を"あなた"に修正する

ログインしたユーザ自身のタスク一覧が表示されている場合は、”〇〇 さんのタスク一覧”でなく、”あなたのタスク一覧”と表示するよう、ついでに変更します。

src/component/TodoList.js

const TodoList = ({displayName, todos, isOwnTodos, onTodoClick}) => {
  // 中略
  const name = isOwnTodos ? 'あなた' : `${displayName} さん`;
  return (
    <div>
      {displayName && <div>{name}のタスク一覧</div>}  {/* 変更 */}
      <ul>
      // 中略
      </ul>
    </div>
  )
}

動作確認

動作確認をします。

ログインしたユーザ以外のタスク一覧を表示します。

f:id:yucatio:20181017214830p:plain

タスク追加フォームが表示されていません。タスクをクリックしてみても、何も起こらず、コンソールにもエラーが表示されません。

ログインしたユーザのタスク一覧を表示します。 タスク追加フォームが表示されています。

f:id:yucatio:20181017215309p:plain

タスクが追加できます。

タスク完了フラグの切り替えを行います。

f:id:yucatio:20181017215950p:plain

タスク完了フラグの切り替えも正常に行えます。

以上でログインしたユーザ自身のタスク以外は変更できないように修正できました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

パスで指定されたユーザIDでタスクを読み込む (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

読み込むuidの変更

現在の実装では、ログインしたユーザのタスク一覧が表示されています。これを、パスで指定されたユーザのタスクを読み込むように変更します。

src/App.jsで、タスク一覧のURLは、/users/:uid/todosと指定しました。この:uidにマッチする部分は、this.props.match.params.uidで取得することができます。

読み込むタスク一覧のuidの指定はTodoComponentで行なっているので、変更します。

src/components/TodoComponent.js

let TodoComponent = ({authenticating, authenticated, match: { params: {uid}}}) => {  // #1
  // 中略
  return (
    <div>
      <AddTodo uid={uid} />
      <NoticeForTodo />
      <VisibleTodoList uid={uid} />
      <Footer />
    </div>
  )
}

TodoComponent.propTypes = {
  // uid: PropTypes.string,  は削除  #2
  authenticating: PropTypes.bool.isRequired,
  authenticated: PropTypes.bool.isRequired,
  match: PropTypes.shape({  // #3
    params: PropTypes.shape({
      uid: PropTypes.string.isRequired
    }).isRequired
  }).isRequired
}

const mapStateToProps = ({firebase: {auth}}) => ({  // #4
  // uid,  は削除 #5
  authenticating:  !isLoaded(auth),
  authenticated: !isEmpty(auth)
})
  • uidを引数から削除し、match: { params: {uid}}を追加します(#1)。
  • ログインしたユーザのuidに関するコードを削除します(#2, #4, #5)。
  • match.params.uidに対応するpropTypeを追加します(#3)。

認証に関する処理を削除

STEP 2では、ログインしていない場合にタスク一覧を表示していませんでしたが、STEP 3では誰でもタスク一覧が見られるので、ログイン状態に関する機能を削除します。 react-reduxのconnectが不要になりますが、次回また必要になるので残しておきます。

src/components/TodoComponent.js

// import { isEmpty, isLoaded } from 'react-redux-firebase'  を削除

let TodoComponent = ({match: { params: {uid}}}) => (  // #1
  // authenticatingやauthenticatedで分岐していた処理を削除。() => (expression) 形式に変更
  <div>
    <AddTodo uid={uid} />
    <NoticeForTodo />
    <VisibleTodoList uid={uid} />
    <Footer />
  </div>
)

TodoComponent.propTypes = {
//   authenticating: PropTypes.bool.isRequired,  を削除
//   authenticated: PropTypes.bool.isRequired,  を削除
  match: PropTypes.shape({
    params: PropTypes.shape({
      uid: PropTypes.string.isRequired
    }).isRequired
  }).isRequired
}

const mapStateToProps = (state) => ({
//   authenticating:  !isLoaded(auth),  を削除
//   authenticated: !isEmpty(auth)  を削除
})

動作確認

Firebaseのコンソールにログインして、Realtime DatabaseでユーザのIDを取得し、URLを手動で作成して動作確認をしましょう。

f:id:yucatio:20181015205411p:plain:w300

f:id:yucatio:20181015210311p:plain

ユーザごとのタスクが表示されました。

ユーザ名の表示

パスで指定されたユーザのタスクは表示されるようになりましたが、誰のタスクかわからないので、ユーザ名を表示するように変更します。

Firebaseからの読み込み部分を作成します。

src/containers/VisibleTodoList.js

const firebaseQueries = ({uid}) => (
  [
    {path: `users/${uid}/displayName`, type: 'once'},  // #1
    `todos/${uid}`
  ]
 )

const mapStateToProps = ({visibilityFilter, firebase: {data : {users, todos}}}, {uid}) => {  // #2
  return {
    displayName: users && users[uid] && users[uid].displayName,  // #3
    todos: getVisibleTodos(todos && todos[uid], visibilityFilter)
  }
}
  • ユーザ名をFirebaseから取得します(#1)。パスはusers/${uid}/displayNameです。変更を検知する必要がないので、type: 'once'で読み込んでいます。
  • stateから読み込む情報に、firebase.data.usersを追加します(#2)。
  • displayNamepropsに渡します(#3)。

表示部分を実装します。

src/components/TodoList.js

const TodoList = ({displayName, todos, onTodoClick}) => {  // #1
  // 中略
  return (
    <div>  // #2
      {displayName && <div>{displayName} さんのタスク一覧</div>}  // #3
      <ul>
        // 中略
      </ul>
    </div>
  }
}

TodoList.propTypes = {
  displayName: PropTypes.string,  // #4
  // 後略
}
  • propsからdisplayNameを取得します(#1)。
  • 親要素としてdivを追加しました(#2)。
  • displayNameが存在する時に、表示します(#3)。
  • displayNameのPropTypeを追加します(#4)。

動作確認

動作確認をします。

f:id:yucatio:20181015214520p:plain

タスクの持ち主の名前が表示されました。

f:id:yucatio:20181015214912p:plain:w250

ユーザが存在しない場合、名前は表示されません。

以上でパスで指定されたユーザIDでタスクを読み込む処理が完成しました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

react-routerを使用したナビゲーションリンクの作成 (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Navbarを実装して、ナビゲーションリンクを作成します。 ルートパス(Home)へのリンクと、ログインしている場合は、自身のタスク一覧へのリンクを表示します。

リンクは、通常はLinkコンポーネントを使用しますが、ナビゲーションには、LinkスペシャルバージョンであるNavLinkを利用します。NavLinkを使用すると、現在表示しているページのリンクだけCSSスタイルを変えたい、という要望が簡単に実現できます。

ルートパス(Home)へのリンクの追加

手始めにHomeへのリンクを追加します。activeStyleを設定して、現在のURLが/のときに文字の太さと文字色を変えています。

src/components/Navbar.js

import React from 'react'
import { NavLink } from 'react-router-dom'

let Navbar = () => (
  <div>
    <NavLink exact to='/' activeStyle={{fontWeight: "bold", color: "#DF3A01"}}>Home</NavLink>
  </div>
)

export default Navbar;

動作確認します。http://localhost:3000/users/aaaaa/todos を表示した後、Homeリンクを押します。

f:id:yucatio:20181013221633p:plain

URLが/に変化し、画面もルート(Home)に遷移して、Homeリンクの文字が赤の太字になっています。

ログイン時、自身のtodosへのリンクの追加

ログイン時に、/users/{ユーザID}/todosへのリンクを表示します。

src/components/Navbar.js

import React from 'react'
import { connect } from 'react-redux'
import { NavLink } from 'react-router-dom'
import PropTypes from 'prop-types'

let Navbar = ({uid}) => (
  <div>
    <NavLink exact to='/' activeStyle={{fontWeight: "bold", color: "#DF3A01"}}>Home</NavLink>&nbsp;
    { uid && <NavLink exact to={`/users/${uid}/todos`} activeStyle={{fontWeight: "bold", color: "#DF3A01"}}>タスクを編集する</NavLink> }
  </div>
)

Navbar.propTypes = {
  uid: PropTypes.string
}

const mapStateToProps = state => (
  { uid: state.firebase.auth.uid }
)

Navbar =connect(
  mapStateToProps
)(Navbar)

export default Navbar;

動作確認してみます。

ログアウト時はタスクへのリンクは表示されません。

f:id:yucatio:20181013222515p:plain

http://localhost:3000/ を開き、 Homeからタスク一覧へ遷移します。

f:id:yucatio:20181013224438p:plain

ルートパスから/users/{ユーザID}/todosに遷移して、todoComponentが描画されましたが、NavLinkの表示が変わりません。/users/{ユーザID}/todosに遷移したときにHomeのスタイルは解除され、タスクを編集するactiveStyleで指定したスタイルが適用されるはずですが。。

NavLinkをconnectと一緒に使用するときは、withRouterを使用する

connectを使用するとスタイルが適用されない件については、 react-router/docs/guides/blocked-updates に記載があります。URLが変更されても、connectで変更が検知されず、下位コンポーネントの再描画がされないことが原因のようです。

この現象を回避するには、connectwithRouterで囲みます。これによって、locationオブジェクトがconnectpropsとして渡されます。locationオブジェクトはページ遷移のたびに書き換えられるので、connectで変更が検知されます。これでうまく動きます。

コードを修正します。

src/components/Navbar.js

import { NavLink, withRouter } from 'react-router-dom' // 変更

// 中略

Navbar = withRouter(connect(
  mapStateToProps
)(Navbar))

// 後略

動作確認をします。再び http://localhost:3000/ を開き、 Homeからタスク一覧へ遷移します。

f:id:yucatio:20181013230103p:plain

Home画面から/users/{ユーザID}/todosに遷移したときに、NavLinkのスタイル切り替えも行われました。

以上でナビゲーションリンクが完成しました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

JavaScriptで「○分前」「○時間前」「○日前」など現在時刻からのざっくりした時間経過を表示したい場合は、Moment.jsのfromNowを使う

やりたいこと

Twitterのように、投稿日付を表示したい。「○分前」「○時間前」「○日前」など、現在時刻からの経過時間をざっくり表示したい。

f:id:yucatio:20181019222047p:plain

moment.jsのfromNowでできる

moment.jsのfromNowを使えば実現できます。

Time from now

デフォルトの設定では以下のように表示されます。未来の日時の場合は、(“前”の部分が”後”になります)

範囲 Key(*1) 表示例
0 から 44 秒 s 数秒前
(ss から 44 秒(*2)) ss 44秒前
45 から 89 秒 m 1分前
90 秒 から 44 分 mm 2分前 ... 44分前
45 から 89 分 h 1時間前
90 分 から 21 時間 hh 2時間前 ... 21時間前
22 から 35 時間 d 1日前
36 時間 から 25 日 dd 2日前 ... 25日前
26 から 45 日 M 1ヶ月前
45 から 319 日 MM 2ヶ月前 ... 10ヶ月前
320 から 547 日 (1年半) y 1年前
548 日以上 yy 2年前 ... 20年前

(*1) Keyは、時間の丸めとなる閾値。例えば、Keyのmは45なので、45分以上は1時間と見なされます。

(*2) ssキーは、デフォルトでは44に設定されているため、”44秒前”のような表示はされません。ssの値を10にしたきには、1から10秒は”数秒前”、11から44秒は”○秒前”と表示されます。

momentjs.com/13-relative-time-threshold.md at master · moment/momentjs.com · GitHub

moment#fromNowを動かしてみる

環境の準備とmomentパッケージのインストール

yarn + ES 6 + React(create-react-app使用)を利用するので、それ以外の環境の場合は適宜読み替えてください。

# 新規プロジェクトの作成
$ yarn create react-app moment-fromnow
yarn create v1.9.4
# 中略
success Saved 10 new dependencies.
info Direct dependencies
├─ react-dom@16.5.2
├─ react-scripts@2.0.5
└─ react@16.5.2
# 中略
Success! Created moment-fromnow at /Users/yuka/react/moment-fromnow
# 後略

$ cd  moment-fromnow

# momentパッケージの追加
$ yarn add moment
# 中略
info All dependencies
├─ moment@2.22.2
├─ react-dom@16.5.2
└─ react@16.5.2

シンプルなコード

簡単なコードを動かしてみます。

src/App.js

import React from 'react';
import moment from 'moment'  // #1
import 'moment/locale/ja'  // #2

const App = () => (
  <div>
    {moment('2018-10-19 13:13:13').fromNow()}  // #3
  </div>
);

export default App;
  • momentをimportします(#1)。
  • moment/locale/jaをimportし、日本語化します(#2)。
  • moment('2018-10-19 13:13:13’)で、201年10月19日13時13分13秒を表すmomentオブジェクトを作成し、fromNow()を呼び出します(#3)。

実行結果

サーバを起動して確認します。

$ yarn start

http://localhost:3000 を開きます。

f:id:yucatio:20181019223439p:plain

無事表示されました。

様々な時刻でfromNowの実行結果を確認する

簡単な動作確認はできたので、一通り全ての範囲の表示を確認します。

src/App.js

import React from 'react';
import moment from 'moment'
import 'moment/locale/ja'
import './App.css';

const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'

const App = () => {
  let now = moment();
  const timeAgoArr = [
    {description: '3秒前'              , timeAgo: moment(now).subtract(3, 'seconds')},
    {description: '57秒前'             , timeAgo: moment(now).subtract(57, 'seconds')},
    {description: '1分7秒前'            , timeAgo: moment(now).subtract(1, 'minutes').subtract(7, 'seconds')},
    {description: '4分55秒前'           , timeAgo: moment(now).subtract(4, 'minutes').subtract(55, 'seconds')},
    {description: '17分25秒前'          , timeAgo: moment(now).subtract(17, 'minutes').subtract(25, 'seconds')},
    {description: '56分2秒前'           , timeAgo: moment(now).subtract(56, 'minutes').subtract(2, 'seconds')},
    {description: '1時間34分22秒前'      , timeAgo: moment(now).subtract(1, 'hours').subtract(34, 'minutes').subtract(22, 'seconds')},
    {description: '10時間49分42秒前'     , timeAgo: moment(now).subtract(10, 'hours').subtract(49, 'minutes').subtract(42, 'seconds')},
    {description: '22時間50分2秒前'      , timeAgo: moment(now).subtract(22, 'hours').subtract(50, 'minutes').subtract(2, 'seconds')},
    {description: '1日13時間30分52秒前'  , timeAgo: moment(now).subtract(1, 'days').subtract(13, 'hours').subtract(30, 'minutes').subtract(52, 'seconds')},
    {description: '22日17時間6分11秒前'  , timeAgo: moment(now).subtract(22, 'days').subtract(17, 'hours').subtract(6, 'minutes').subtract(11, 'seconds')},
    {description: '31日20時間17分55秒前' , timeAgo: moment(now).subtract(31, 'days').subtract(20, 'hours').subtract(17, 'minutes').subtract(55, 'seconds')},
    {description: '106日9時間5分1秒前'   , timeAgo: moment(now).subtract(106, 'days').subtract(9, 'hours').subtract(5, 'minutes').subtract(1, 'seconds')},
    {description: '342日21時間28分14秒前', timeAgo: moment(now).subtract(342, 'days').subtract(21, 'hours').subtract(28, 'minutes').subtract(14, 'seconds')},
    {description: '1036日4時間2分53秒前' , timeAgo: moment(now).subtract(1036, 'days').subtract(4, 'hours').subtract(2, 'minutes').subtract(53, 'seconds')},
  ]
  return (
    <div className="fromNowTable">
      <div className="currentTime">現在時刻: {now.format(DATE_FORMAT)}</div>
      <table>
        <thead>
          <tr>
            <th>説明</th><th>日時</th><th>moment().fromNow()</th>
          </tr>
        </thead>
        <tbody>
          {timeAgoArr.map(({description, timeAgo}) => (
            <tr>
              <td className="description">{description}</td>
              <td>{timeAgo.format(DATE_FORMAT)}</td>
              <td>{timeAgo.fromNow()}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

export default App;

実行結果

$ yarn start

http://localhost:3000 を開いて確認します。

f:id:yucatio:20181019223852p:plain

それぞれ”5分前”や”2時間前”、”23日前”など表示されることが確かめられました。

あとがき

Ruby on Railsのtime_ago_in_wordsを探していたのですが、全然見つからず苦労しました。危うく自分で実装するところでした。

ちなみにTwitterfacebookは24時間以内の投稿は、○[分/時間]前、それ以上前だと投稿した日付が表示されます。SNSなどはその方が良いかもしれませんね。

環境

関連記事

yucatio.hatenablog.com