ReactでinputRef使用時、参照を保持する変数がundefinedになった原因と対処法

ReactでinputRef使用時、参照を保持する変数がundefinedになり、エラーが発生しました。その原因と対処法を紹介します。

先に結論

参照を保持する変数は、ローカル変数でなく、インスタンス変数にします。インスタンス変数はReact.Componentを拡張したクラスで使用できます(関数コンポーネントでは使用できません)。

良い例

import TextField from '@material-ui/core/TextField'

class AddTodo extends React.Component {
  render() {
    return (
      <TextField
        inputRef={node => {
          this.inputElement = node
        }}
      />
    )
  }
}

よくない例

import TextField from '@material-ui/core/TextField'

const AddTodo = ({uid, dispatch}) => {
  let input;

  // 略
  return (
    <TextField
      inputRef={node => {
        input = node
      }}
    />
  )
}

エラーが発生するコード

以下のコードは、Redux公式に載っているreact-reduxのサンプルを変更したものです。

propsにuid(ユーザID)とsending(送信中を表す真偽値)を追加しています。 Material-UIを利用し、 <input><TextField><button><Button>に変更しています。sendingtrue(データを送信中)の時は、<TextField><Button>をdisabledにしています。<TextField>のrefをinputRefに変更した理由については以下のリンクをご覧ください。

yucatio.hatenablog.com

src/AddTodo.js

import Button from '@material-ui/core/Button'
import TextField from '@material-ui/core/TextField'
// 他のimportは略

const AddTodo = ({uid, sending, dispatch}) => {
  let input;

  return (
    <div>
      <form
        onSubmit={ e => {
          e.preventDefault()
          if (!input.value.trim()) {
            return
          }
          dispatch(addTodo(uid, input.value))
          input.value = ''
        }}
      >
        <TextField
          inputRef={node => {
            input = node
          }}
          disabled={sending}
        />
        {' '}
        <Button variant="contained" type="submit" disabled={sending}>
          タスクを追加
        </Button>
      </form>
    </div>
  )
}

// 以下略

動作イメージ

テキストフィールドに文字列を入力して、タスクを追加ボタンを押します。

f:id:yucatio:20181118232815p:plain

サーバにデータにデータを送信中は上記コードのsendingtrueになり、テキストフィールドとボタンが操作できなくなります。

f:id:yucatio:20181118232833p:plain

サーバへのデータ送信が完了すると、sendingfalseになり、入力が可能になります。

f:id:yucatio:20181118232847p:plain

エラー再現手順

  1. ページを表示する
  2. タスクを追加する
  3. 再びタスクを追加する

2回目のタスク追加で、以下のようなエラーが表示されます。inputの値がundefinedになっています。

TypeError: Cannot read property 'value' of undefined

f:id:yucatio:20181118233211p:plain

原因究明

確認のためのログを追記してもう一度実行します。

src/AddTodo.js

// 前略

const AddTodo = ({uid, sending, dispatch}) => {
  console.log('render')  // 追加
  let input;
  console.log('input', input)  // 追加
  console.log('sending', sending)  // 追加

  return (
    <div>
      <form
        onSubmit={ e => {
          e.preventDefault()
          console.log('in onSubmit')  // 追加
          console.log('input', input)  // 追加
          if (!input.value.trim()) {
            return
          }
          dispatch(addTodo(uid, input.value))
          input.value = ''
        }}
      >
        <TextField
          inputRef={node => {
            console.log('in inputRef callback')  // 追加
            input = node
            console.log('input', input)  // 追加
          }}
          disabled={sending}
        />
        {' '}
        <Button variant="contained" type="submit" disabled={sending}>
          タスクを追加
        </Button>
      </form>
    </div>
  )
}

// 後略

再びエラー再現

1. ページを表示する

renderが呼ばれ、refコールバックが呼ばれます。input変数は、宣言直下ではundefinedですが、refコールバック内でnodeがセットされます。

f:id:yucatio:20181118234857p:plain

2. タスクを追加する

sendingの値が変化したので、renderが呼ばれ、input変数が再びundefinedに変更されます。この時refコールバックは呼ばれません。ここがバグの原因のようです。

f:id:yucatio:20181118234913p:plain

3. 再びタスクを追加する

エラーが表示されました。onSubmit中でもinputは undefinedのままです。

f:id:yucatio:20181118234930p:plain

修正

Reactの公式サンプル (Refs and the DOM – React) にあるように、参照を保持する変数をインスタンス変数 (this.inputElement)に変更します。

インスタンス変数を使用するために、関数コンポーネントからReact. Componentを拡張したクラスに変更します。

// 前略

class AddTodo extends React.Component {  // 変更
  render() {  // 変更
    const {uid, sending, dispatch} = this.props  // 変更

    return (
      <div>
        <form
          onSubmit={ e => {
            e.preventDefault()
            if (!this.inputElement.value.trim()) {  // 変更
              return
            }
            dispatch(addTodo(uid, this.inputElement.value))  // 変更
            this.inputElement.value = ''  // 変更
          }}
        >
          <TextField
            inputRef={node => {
              this.inputElement = node  // 変更
            }}
            disabled={sending}
          />
          {' '}
          <Button variant="contained" type="submit" disabled={sending}>
            タスクを追加
          </Button>
        </form>
      </div>
    )
  }
}

// 後略

動作確認

  1. ページを表示する
  2. タスクを追加する
  3. 再びタスクを追加する

2回目のタスク追加でもエラーなくタスクを追加できました。

f:id:yucatio:20181118235656p:plain

<input>がrender内に書いてある場合

Redux公式のコードでは、<input>がAddTodoのrender内に書いてあります。このように書いてあると、AddTodoのrenderが呼ばれるたびにrefコールバックが実行されます。そのため、2回目以降もinput変数に値が入っている状態になり、エラーにはなりません。しかし、TextFieldに変更すると初回のしかrefコールバックが実行されないため、エラーになってしまいました。

あとがき

2回目のタスク追加でエラーになるとは予想外でした。見つけにくいバグなので2回連続でタスク追加するパターンをテストケースに追加しようと思いました。


Material-UIのTextFieldでrefを使う

TextFieldでrefを使う

Material-UI導入時、inputをTextFieldに置き換える時に、多くの属性は<input><TextField>に置き換えた際そのままでよいのですが、refはそのままにすると動きません。refinputRefに変更する必要があります。

変更前

const AddTodo = ({dispatch}) => {
  let input;

  // 略
  return (
    <input
      ref={node => {
        input = node
      }}
    />
  )
}

変更後

import TextField from '@material-ui/core/TextField'

class AddTodo extends React.Component {
  render() {
    return (
      <TextField
        inputRef={node => {
          this.inputElement = node
        }}
      />
    )
  }
}

<TextField><input>そのものではないので、TextFieldにrefを指定しても動きません。代わりに、inputRefプロパティを渡して、(TextFieldから子孫コンポーネンントにバケツリレーして、)<input ref={props.inputRef}>としてrefを設定します。

同様に、inputにプロパティ(readonlyやstepなど)を渡す場合にも、inputPropsにオブジェクトを渡します。

関数コンポーネントからクラスに変更した理由と、node => { this.inputElement = node}に変更した理由はこちらの記事を参照してください。

yucatio.hatenablog.com

refの使用

inputRefの使用については、 React公式ページの例がわかりやすいと思います。

Callback Refs

You can pass callback refs between components like you can with object refs that were created with React.createRef().

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

In the example above, Parent passes its ref callback as an inputRef prop to the CustomTextInput, and the CustomTextInput passes the same function as a special ref attribute to the <input>. As a result, this.inputElement in Parent will be set to the DOM node corresponding to the <input> element in the CustomTextInput.

Refs and the DOM – React

"React.createdRef()でrefを作成したのと同じように、コンポーネント間でコールバックrefを渡すことができます。

上記の例では、親コンポーネントはCustomTextInputにrefコールバックをinputRefプロパティとして渡し、CustomTextInputはinputRefで受け取った関数を<input>のref属性に設定しています。そのため、親コンポーネントのthis.inputElementはCustomTextInputの<input>のDOMノードが設定されることになります。"

あとがき

refが何であるかきちんと理解しないままinputをTextFieldに置き換えていたので、ハマりました。公式ドキュメント(Refs and the DOM – React) を読んでrefの理解が深まりました。

バージョン

  • @material-ui/core: 3.4.0

react-redux-firebaseで、pushの自動生成key(ID)を事前に取得

firebaseでpushのkeyを事前取得

firebaseの公式サイトには、key(ID)の取得は以下のように書いてあります。

// Get a key for a new Post.
var newPostKey = firebase.database().ref().child('posts').push().key;

react-redux-firebaseでも同様に、

const firebase = getFirebase();
const id = firebase.push('path/to/data').key

でkey(ID)を取得することができます。pushの第1引数にパスを指定して、第2引数は省略します。 firebase.push(‘path/to/data’)はサーバへ通信を行わないので、オフライン時にも結果がすぐに返ってきます。

firebase.push('path/to/data').keyでkey(ID)を取得した後、firebase.set(`path/to/data/${id}`,obj)を実行することで、先に取得したkey(ID)にデータを登録することができます。

keyの事前取得は、上の公式ページのように同一キーで複数のパスに書き込む場合や、以下で示すように新規作成されたデータがサーバにコミット(データが登録)されたかどうかを表示する場合に使用します。

使用例: リストに追加(新規作成)されたデータがサーバに送信中かを表示したい

タスク管理アプリにて、 pushしたデータの送信ステータスをデータごとに表示します。 リストにタスクを追加すると、送信中を示す矢印を表示し、コミット(サーバへのデータ登録)が完了すると矢印を消去します。エラーが起こった場合は、警告マークを表示します。

動作イメージ↓

f:id:yucatio:20181114145054p:plain

action

actionの実装例です。

// reduxのaction
export const addTodo = (uid, text) => {  // uidはユーザID
  // redux-thunk
  return (dispatch, getState, {getFirebase}) => {
    const firebase = getFirebase();
    // key(id)の取得
    const id = firebase.push(`todos/${uid}`).key;

    // “送信中”の表示をする
    dispatch({type: ADD_TODO_REQUEST, todoId: id});
    // 取得したidでデータをセットする
    firebase.set(`todos/${uid}/${id}`,{
        completed: false,
        text,
    }).then(() => {
      // 表示をクリアする
      dispatch({type: ADD_TODO_SUCCESS, todoId: id});
    }).catch(err => {
      // エラーを表示する
      dispatch({type: ADD_TODO_ERROR, todoId: id, err});
    });
  }
}

reducer

reducerの実装例です。

const todoStatuses = (state = {}, action) => {
  switch (action.type) {
    case ADD_TODO_REQUEST:
      return { ...state, [action.todoId]: {status : 'sending', message: '送信中'}}
    case ADD_TODO_SUCCESS:
      return { ...state, [action.todoId]: {status : 'success'}}
    case ADD_TODO_ERROR:
      return { ...state, [action.todoId]: {status : 'error', message: 'エラーが発生しました。時間をおいて再度お試しください。'}}
    default:
      return state
  }
}

export default todoStatuses

あとは state.todoStatuses[todoId].statusの値によって表示を出し分ければ完了です。

あとがき

keyの取得方法はどうやらfirebaseのバージョンによって.key.key()のどちらを使用するか異なるので、片方でうまくいかなかったらもう片方を試してみてもよいかと思います。

redux-thunkとreact-redux-firebaseのつなぎ方は、こちらの記事を参照ください。

yucatio.hatenablog.com

バージョン

  • react-redux-firebase: 2.1.8

Material-UIのwithStyleとreacrt-reduxのconnectを同時に使う

Material-UIのwithStyleとreacrt-reduxのconnect

react-reduxを利用しているアプリにMaterial-UIのstylesを組み込む、もしくはその逆を行うときに、どのようにすれば分からなかったので書いておきます。

connectを使用したコンポーネント

import React from 'react'
import { connect } from 'react-redux'

const SampleComponent = () => {
  // 略
}

const mapStateToProps = (state) => ({
  // 略
})

const mapDispatchToProps = (dispatch) => ({
  // 略
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(SampleComponent)

withStylesを使用したコンポーネント

import React from 'react'
import { withStyles } from '@material-ui/core/styles'

const SampleComponent = () => {
  // 略
}

const styles = theme => ({
  // 略
})

export default withStyles(styles)(SampleComponent)

を、同時に使いたい。

composeを使う

reduxのcompose関数を使用すると、withStyleとconnectを同時に使用することができます。

import React from 'react'
import { compose } from 'redux'
import { connect } from 'react-redux'
import { withStyles } from '@material-ui/core/styles'

const SampleComponent = () => {
  // 略
}

const styles = theme => ({
  // 略
})

const mapStateToProps = (state) => ({
  // 略
})

const mapDispatchToProps = (dispatch) => ({
  // 略
})

export default compose(
  withStyles(styles),
  connect(
    mapStateToProps,
    mapDispatchToProps
))(SampleComponent)

composeを使わない場合

compose関数は、関数を合成してくれる関数です。合成とは、”他の関数の戻り値”を入力とする関数を作成することです。 例えば、compose(a, b)(x)は、a(b(x))と同じ意味になります。

composeを使わない場合は以下のように書くことができます。

// 略

export default withStyles(styles)(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(SampleComponent)
)

composeを使うよりネストが深くなりました。

バージョン

  • react: 16.5.0
  • react-redux: 5.0.7
  • redux: 4.0.0
  • @material-ui/core: 3.4.0

Firebaseアプリの公開 (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

アプリが完成したので、Firebaseにアプリを公開します。 ビルドします。

$ yarn run build

一旦ローカルで正常に動くか確認しましょう。--only hostingオプションで、hostingのみ動作するようにします。(functionはserveしない)

$ firebase serve --only hosting

http://localhost:5000にアクセスして、動作確認しましょう。 確認が終わったら、Firebaseにデプロイします。functionsは既にデプロイしてあるので、hostingのみデプロイします。

$ firebase deploy --only hosting

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

i  deploying hosting
i  hosting[todo-sample-xxxxx]: beginning deploy...
i  hosting[todo-sample-xxxxx]: found 7 files in build
✔  hosting[todo-sample-xxxxx]: file upload complete
i  hosting[todo-sample-xxxxx]: finalizing version...
✔  hosting[todo-sample-xxxxx]: version finalized
i  hosting[todo-sample-xxxxx]: releasing new version...
✔  hosting[todo-sample-xxxxx]: release complete
✔  Deploy complete!

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

無事デプロイできたので、https://todo-sample-xxxxx.firebaseapp.com にアクセスします。(todo-sample-xxxxは自身のプロジェクトIDに置き換えてください。)

f:id:yucatio:20181102121736p:plain

アプリが公開できました!

ログインしたユーザへのタスクの追加・更新と、ログインしたユーザとは別のユーザのタスクを見ることができました。

これでSTEP 3は終了です。本格的なアプリになってきましたね。

★次回の記事

★目次

yucatio.hatenablog.com


リファクタリング: 作成日時と更新日付の追加 (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Firebase Realtime Databaseでは、データの作成日時・更新日時は自動で付与されないので、クライアント側(javascript)で付与します。これに伴い、Cloud FunctionsもDatabaseの更新日時を使用するように変更します。データの作成日時・更新日時は、サンプルのアプリでは特に必要がないですが、アプリをリリースした後、問い合わせがあった場合の調査などに重要になります。

actionの更新

タスク作成時に、作成時間_createdAt_updatedAtを付与し、更新時に_updatedAtを更新します。 時刻関連の処理には、使いやすさのため、momentモジュールを使用します。

src/actions/todoActions.js

import moment from 'moment'  // 追加
// 中略

export const addTodo = (uid, text) => {
  return (dispatch, getState, {getFirebase}) => {
    // 中略
    const createdAt = moment().valueOf();  // 追加
    firebase.push(`todos/${uid}`,{
      completed: false,
      text,
      _createdAt : createdAt,  // 追加
      _updatedAt : createdAt  // 追加
    })
    // 中略
  }
}

export const toggleTodo = (uid, id) => {
  return (dispatch, getState, {getFirebase}) => {
    // 中略
    const updatedAt = moment().valueOf();  // 追加
    firebase.update(`todos/${uid}/${id}`, {
      completed: ! todo.completed,
      _updatedAt : updatedAt  // 追加
    })
    // 中略
  }
}

database.rules.jsonの更新

_createdAt_updatedAtを登録できるようにデータベースルールファイルを変更します。

database.rules.json

{
  "rules": {
    "todos": {
      "$uid": {
        ".read": true,
        ".write": "$uid === auth.uid",
        "$todoId": {
          ".validate": "newData.hasChildren(['text', 'completed', '_createdAt', '_updatedAt'])",  // 変更
          "text": {
            ".validate": "newData.isString()"
          },
          "completed": {
            ".validate": "newData.isBoolean()"
           },
          "_createdAt": {  // 追加
            ".validate": "newData.isNumber()"
          },
          "_updatedAt": {  // 追加
            ".validate": "newData.isNumber()"
          },
          "$other": { ".validate": false }
        }
      }
    },
    // 中略
  }
}

変更を反映します。

firebase deploy --only database

".validate": "newData.hasChildren(['text', 'completed', '_createdAt', '_updatedAt’])”があるせいで、既存のタスクが更新できなくなってしまいました。今回は既存のタスクは削除します。次回からは初めから_createdAt_updatedAtは付与しましょう。

Cloud Functionsの修正

タスクの作成トリガを/todos/{uid}/{todoId}/_createdAtの作成、更新トリガを/todos/{uid}/{todoId}/_updatedAtの更新とするように変更します。 また、recentUpdatedTodos_updatedAtは、todos_updatedAtから取得するように変更します。

fuctions/index.js

const addRecentUpdate = (uid, todoId, todo, eventType) => {  // #1
  return admin.database().ref('/users/' + uid + '/displayName').once('value').then((snapshot) => {
    const displayName = snapshot.val();
    return (admin.database().ref('/recentUpdatedTodos/' + todoId).set({
      uid,
      displayName,
      text: todo.text,  // #2
      eventType,
      _updatedAt: todo._updatedAt  // #3
    }));
  });
}

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

    return snapshot.ref.parent.once('value').then((snapshot) => {  // #5
      const todo = snapshot.val();
      return addRecentUpdate(uid, todoId, todo, 'CREATE');
    });
})

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

    return change.after.ref.parent.once('value').then((snapshot) => {  // #7
      const todo = snapshot.val();
      return addRecentUpdate(uid, todoId, todo, 'UPDATE');
    });
})
  • addRecentUpdateの引数のうち、texttodoに変更します(#1)。
  • texttodo.textで取得します(#2)。
  • recentUpdatedTodos_updatedAtを、todo_updatedAtから取得します(#3)。
  • データパスを、/todos/{uid}/{todoId}/textから/todos/{uid}/{todoId}/_createdAtに変更します(#4)。
  • 親ノードを取得する処理を追加します(#5)。
  • 関数名を、addRecentUpdateOnUpdateCompletedからaddRecentUpdateOnUpdateに変更し、データパスを、/todos/{uid}/{todoId}/completedから/todos/{uid}/{todoId}/_updatedAtに変更します(#6)。
  • textノードを取得する処理から、親ノードを取得する処理に変更します(#7)。

デプロイします。

$ 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 (56.34 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating Node.js 6 function addRecentUpdateOnUpdate(us-central1)...
i  functions: updating Node.js 6 function addRecentUpdateOnCreate(us-central1)...
i  functions: updating Node.js 6 function limitRecentUpdatedTodos(us-central1)...

今回、functionの名前を変更したので、途中でaddRecentUpdateOnUpdateCompletedを削除するか聞かれます。”y”を選択します。

The following functions are found in your project but do not exist in your local source code:
    addRecentUpdateOnUpdateCompleted(us-central1)

If you are renaming a function or changing its region, it is recommended that you create the new function first before deleting the old one to prevent event loss. For more info, visit https://firebase.google.com/docs/functions/manage-functions#modify
? Would you like to proceed with deletion? Selecting no will continue the rest o
f the deployments. Yes
i  functions: deleting function addRecentUpdateOnUpdateCompleted(us-central1)...
✔  functions[addRecentUpdateOnUpdateCompleted(us-central1)]: Successful delete operation. 
✔  functions[addRecentUpdateOnCreate(us-central1)]: Successful update operation. 
✔  functions[limitRecentUpdatedTodos(us-central1)]: Successful update operation. 
✔  functions[addRecentUpdateOnUpdate(us-central1)]: Successful create operation. 

✔  Deploy complete!

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

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

動作確認

動作確認をします。

タスクを新規登録します。

f:id:yucatio:20181101144012p:plain:w200

データベースを確認します。

f:id:yucatio:20181101145256p:plain

_createdAt_updatedAtが追加されています。

recentUpdatedTodos_updatedAttodosのものと同じ時刻です。

タスクを更新します。

f:id:yucatio:20181101144642p:plain:w200

データベースを確認します。

f:id:yucatio:20181101144900p:plain

_updatedAtが更新されました。recentUpdatedTodos_updatedAttodosの時刻で更新されています。

以上で作成日時と更新日付の追加の処理の完了です。Firebase Realtime Databaseを使用する際は、最初から全てのデータに作成日時と更新日時を付与することを強くおすすめします。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com


リファクタリング : componentsとcontainersの階層化 (STEP 3 : 他のユーザのタスクが見れるタスク管理アプリを作成する - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

componentのファイルが多くなり、管理しづらくなってきたので、画面パーツごとにディレクトリを分けます。

現在のディレクトリ構成と変更後のディレクトリ構成です。

f:id:yucatio:20181029142629p:plain

階層の移動と構成

以下に、src/components以下のファイルの移動先を示します。

ファイル名 移動先
App.js App.js (変更なし)
NoMatch.js NoMatch.js (変更なし)
Dashboard.js dashboard/index.js
RecentUpdatedTodos.js dashboard/recentUpdatedTodos/index.js
UserUpdatedTodo.js dashboard/recentUpdatedTodos/UserUpdatedTodo.js
Login.js login/index.js
Navbar.js navbar/index.js
Footer.js todos/Footer.js
Link.js todos/Link.js
NoticeForTodo.js todos/Notice.js
Todo.js todos/Todo.js
TodoList.js todos/TodoList.js
TodoComponent.js todos/index.js

同様にsrc/containersも移動します。

ファイル名 移動先
AddTodo.js todos/AddTodo.js
FilterLink.js todos/FilterLink.js
VisibleTodoList.js todos/VisibleTodoList.js

ソースコードの修正

階層を変更したので、ソースコード中のパスも書き換えます。

src/components/App.js

import Login from './login/'
import Navbar from './navbar/'
import Dashboard from './dashboard/'
import TodoComponent from './todos/'

src/components/dashboard/index.js

import RecentUpdatedTodos from './recentUpdatedTodos/'

src/components/login/index.js

import { loginWithGoogle, logout } from '../../actions/authActions'

src/components/todos/Footer.js

import FilterLink from '../../containers/todos/FilterLink'
import { VisibilityFilters } from '../../actions/visibilityFilterActions'

src/components/todos/index.js

// 前略
import { locationChangeOnTodos } from '../../actions/todoActions'
import Footer from './Footer'
import Notice from './Notice'
import AddTodo from '../../containers/todos/AddTodo'
import VisibleTodoList from '../../containers/todos/VisibleTodoList'

class TodoComponent extends React.Component {
  // 中略
  render() {
    // 中略
    return (
      <div>
        {isOwnTodos && <AddTodo uid={uid} />}
        <Notice />  {/* 変更 */}
        <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} />
        <Footer />
      </div>
    )
  }
}

src/containers/todos/AddTodo.js

import { addTodo } from '../../actions/todoActions'

src/containers/todos/FilterLink.js

import { setVisibilityFilter } from '../../actions/visibilityFilterActions'
import Link from '../../components/todos/Link'

src/containers/todos/VisibleTodoList.js

import { toggleTodo } from '../../actions/todoActions'
import TodoList from '../../components/todos/TodoList'

以上でcomponentsとcontainersの階層化ができました。1つのディレクトリのファイル数が10を超えたらディレクトリ分割を検討するとよいです。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com