yucatio@システムエンジニア

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

Cloud Functions for Firebase用の最初のコードの記述とデプロイ (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Cloud Functions for Firebaseで最初に小さいコードを書いて動作確認をします。 手始めに、DBの/todos/{uid}の下にタスクが追加された時と、タスクが更新された時にコンソールへメッセージを表示します。

トリガの動作確認用コードの作成

/todos/{uid}/の下にタスクが追加された時と、タスクが更新された時にコンソールにメッセージを表示します。

functions/index.js

const functions = require('firebase-functions');  // #1
const admin = require('firebase-admin');  // #2
admin.initializeApp();  // #3

exports.addRecentUpdateOnCreate = functions.database.ref('/todos/{uid}/{todoId}/text')  // #4
  .onCreate((snapshot, context) => {
    const uid = context.params.uid;  // #5
    const todoId = context.params.todoId;  // #6

    console.log('addRecentUpdateOnCreate called. uid=' + uid + ', todoId=' + todoId);  // #7

    return null;  // #8
})

exports.addRecentUpdateOnUpdateCompleted = functions.database.ref('/todos/{uid}/{todoId}/completed')  // #9
  .onUpdate((change, context) => {
    const todoId = context.params.todoId;
    const uid = context.params.uid;

    console.log('addRecentUpdateOnUpdateCompleted called. uid=' + uid + ', todoId=' + todoId);

    return null;
})
  • firebase-functionsを使用します(#1)。このモジュールはCloud Functionを使用するのに必要です。
  • firebase-adminを使用します(#2)。このモジュールはFirebase Realtime Databaseを利用するのに必要です。
  • admin.initializeAppでadminを初期化します(#3)。
  • タスクが追加された時に実行される関数を定義します(#4)。functions.database.ref('/todos/{uid}/{todoId}/text’).onCreate()/todos/{uid}/{todoId}/text’が新規作成された時に、onCreateの引数で与えた関数を実行します。パスは、/todos/{uid}/{todoId}でなく、/todos/{uid}/{todoId}/textです。なるべく深い所にトリガを設定します。
  • pathで波括弧で囲った部分はパラメータとして扱われ、context.params.{パラメータ名}で取得することができます(#5, #6)。
  • console.logで、Firebaseのコンソールにログを出力することが可能になります(#7)。
  • 関数はPromiseか値を返す必要があります。特に返す値がない場合はnullでよいでしょう(#8)。処理が完了した時に何も返さないとエラーになります。
  • タスクが更新された時に実行される関数を定義します(#9)。今回はcompletedの値しか変化しないので、pathは/todos/{uid}/{todoId}/completedです。

Cloud Functionsのデプロイ

functions/index.jsをデプロイします。

$ cd todo-sample
$ firebase deploy --only functions

=== Deploying to 'todo-sample-xxxxx’…

i  deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint

> functions@ lint /Users/yuka/react/todo-sample/functions
> eslint .

✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (55.94 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 6 function addRecentUpdateOnCreate(us-central1)...
i  functions: creating Node.js 6 function exports.addRecentUpdateOnUpdateCompleted(us-central1)...
✔  functions[addRecentUpdateOnCreate(us-central1)]: Successful create operation. 
✔  functions[exports.addRecentUpdateOnUpdateCompleted(us-central1)]: Successful create operation. 

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/todo-sample-xxxxx/overview

デプロイが完了しました。

Firebaseコンソールでデプロイした関数を確認します。

f:id:yucatio:20181023132227p:plain

動作確認

タスクを追加します。

f:id:yucatio:20181023132805p:plain:w200

コンソールを確認します。

f:id:yucatio:20181023133600p:plain

addRecentUpdateOnCreate called.と、uidとtodoIdが表示されていることが確認できました。

タスクを更新します。

f:id:yucatio:20181023134415p:plain:w200

コンソールを確認します。

f:id:yucatio:20181023133909p:plain

addRecentUpdateOnUpdateCompleted called.と、uidとtodoIdが表示されていることが確認できました。

以上で、/todos/{uid}の下にタスクが追加された時と、タスクが更新された時にコンソールへメッセージを表示することができました。

作成トリガと更新トリガは、更新日時を使用するのがおすすめ

今回は、タスクの作成トリガは/todos/{uid}/{todoId}/textの作成、更新トリガは/todos/{uid}/{todoId}/completedの更新としました。問題なく動きますが、将来的に項目が増えた時にトリガを追加し忘れる恐れがあります。 また、データごとにトリガになるプロパティが違うのは面倒です。

この問題を解決するには、すべてのデータに、作成日付を表す_createdAtプロパティと更新日付を表す_updatedAtプロパティを付与します。データの作成は、_cretedAtの作成をトリガにし、データの更新は、_updatedAtの更新をトリガにします。

以下の記事でこれの対応をしています。

yucatio.hatenablog.com

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

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