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>
に変更しています。sending
がtrue
(データを送信中)の時は、<TextField>
と<Button>
をdisabledにしています。<TextField>
のrefをinputRefに変更した理由については以下のリンクをご覧ください。
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> ) } // 以下略
動作イメージ
テキストフィールドに文字列を入力して、タスクを追加ボタンを押します。
サーバにデータにデータを送信中は上記コードのsending
がtrue
になり、テキストフィールドとボタンが操作できなくなります。
サーバへのデータ送信が完了すると、sending
がfalse
になり、入力が可能になります。
エラー再現手順
- ページを表示する
- タスクを追加する
- 再びタスクを追加する
2回目のタスク追加で、以下のようなエラーが表示されます。inputの値がundefined
になっています。
TypeError: Cannot read property 'value' of undefined
原因究明
確認のためのログを追記してもう一度実行します。
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がセットされます。
2. タスクを追加する
sendingの値が変化したので、renderが呼ばれ、input変数が再びundefinedに変更されます。この時refコールバックは呼ばれません。ここがバグの原因のようです。
3. 再びタスクを追加する
エラーが表示されました。onSubmit中でもinputは undefinedのままです。
修正
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> ) } } // 後略
動作確認
- ページを表示する
- タスクを追加する
- 再びタスクを追加する
2回目のタスク追加でもエラーなくタスクを追加できました。
<input>がrender内に書いてある場合
Redux公式のコードでは、<input>
がAddTodoのrender内に書いてあります。このように書いてあると、AddTodoのrenderが呼ばれるたびにrefコールバックが実行されます。そのため、2回目以降もinput変数に値が入っている状態になり、エラーにはなりません。しかし、TextFieldに変更すると初回のしかrefコールバックが実行されないため、エラーになってしまいました。
あとがき
2回目のタスク追加でエラーになるとは予想外でした。見つけにくいバグなので2回連続でタスク追加するパターンをテストケースに追加しようと思いました。
バージョン
- react : 16.5.0
- @material-ui/core : 3.4.0