reactnative.dev
公式ドキュメントを見ると、TextInput
は13種類のコールバック関数を渡せるらしい。
onBlur
onChange
onChangeText
onContentSizeChange
onEndEditing
onPressIn
onPressOut
onFocus
onKeyPress
onLayout
onScroll
onSelectionChange
onSubmitEditing
TextInput
、たまに触る時にちょろちょろ公式ドキュメントを読んでいるだけで、これらのコールバックの細かい違いを把握していない・・・。
ちゃんと理解した方がいいよね、ということでそれぞれの挙動を実際に動かしながら確認してみました。
(本当はreact native自体の実装も読んだ方が良かったのですが、この記事をアドベントカレンダー予約日前日に書き始めてしまったので叶わぬ夢となりました。すみません。)
実際に動かしたサンプルコードは以下の通りで、観察したいもの以外のpropsを適宜コメントアウトしております。
import { TextInput, View } from "react-native";
export default function App() {
const onBlur = (e) => {
console.log("onBlur");
};
const onChange = (e) => {
console.log("onChange");
};
const onChangeText = (e) => {
console.log("onChangeText");
};
const onContentSizeChange = (e) => {
console.log("onContentSizeChange");
};
const onEndEditing = (e) => {
console.log("onEndEditing");
};
const onPressIn = (e) => {
console.log("onPressIn");
};
const onPressOut = (e) => {
console.log("onPressOut");
};
const onFocus = (e) => {
console.log("onFocus");
};
const onKeyPress = (e) => {
console.log("onKeyPress");
};
const onLayout = (e) => {
console.log("onLayout");
};
const onScroll = (e) => {
console.log("onScroll");
};
const onSelectionChange = (e) => {
console.log("onSelectionChange");
};
const onSubmitEditing = (e) => {
console.log("onSubmitEditing");
};
return (
<View
style={{
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
}}
>
<TextInput
style={{
width: 200,
maxHeight: 200,
borderColor: "gray",
borderWidth: 1,
}}
onBlur={onBlur}
onChange={onChange}
onChangeText={onChangeText}
onContentSizeChange={onContentSizeChange}
onEndEditing={onEndEditing}
onPressIn={onPressIn}
onPressOut={onPressOut}
onFocus={onFocus}
onKeyPress={onKeyPress}
onLayout={onLayout}
onScroll={onScroll}
onSelectionChange={onSelectionChange}
onSubmitEditing={onSubmitEditing}
/>
</View>
);
}
また、reactはv18.2.0、react-nativeはv0.72.6を使用しています。
onPressIn
, onPressOut
, onFocus
onPressIn: ({nativeEvent: PressEvent}) => void
= TextInput
の内部がタップされたとき
onPressOut: ({nativeEvent: PressEvent}) => void
= onPressIn
の後、タップされている指がはなれた時
onFocus:({nativeEvent: LayoutEvent}) => void
= TextInput
にフォーカスが当たった時
ということで、よく使うし名前を見れば直感的に理解ができます。
また、これらの呼ばれる順番ですが、基本的にはonPressIn
,onFocus
,onPressOut
の順番で呼ばれるようです。
簡単ですね。
って思ったんですけど、どうやらonPressOut
は指を離さなくても、画面に触れたままTextInput
の外に指を動かせば発火するようでした。
また、そのまま指を再度TextInput
の中にいれるとonPressIn
も発火するようです。
そして、TextInput
の外で指が離れた場合、TextInput
へフォーカスは当たらず、onFocus
は発火しないみたいでした。
知らなかった。
また、条件の詳細がわかっていないのですが、Androidで前述した操作を行いonPressIn
,onPressOut
を繰り返し発生させてから指を離すと、
「TextInput
にフォーカスは当たっていないがテキスト貼り付けができる」状態になることがあるみたいです。
困るシチュエーションは少なそうですが、バグっぽい挙動なのでReact NativeにPR送るチャンスなのかもしれません。
今度issueやコード見てみようかな。
onChange
,onChangeText
,onKeyPress
onChange:({nativeEvent: {eventCount, target, text}}) => void
= TextInput
の中身のテキストが変わった時に呼ばれる
onChangeText:(text: string) => void
= TextInput
の中身のテキストが変わった時に呼ばれる。引数には変更後のテキストだけが渡される
onKeyPress: ({nativeEvent: {key: keyValue} }) => void
= キーボードが押された時に呼ばれる。
名前から想像がつきますね。
大体のケースで、中身の変更を捕捉したい時はonChangeText
で事足りるように思います。
onChange
を使うと、コンポーネントがマウントされてから何回テキストが変更されたかの情報がeventCount
から取得できるので、
テキストの変更履歴を一定間隔で保存しておく、のような仕様で実装したい時には使えるかもしれません。
onKeyPress
は、キーボードが押下された時に発火するようです。
文字だけでなく、Enter
とBackspace
が押された時にも発火するので、エンターキーが押された時に特定の処理を差し込みたい!
という時に便利かもしれないですね。
ちなみに、キーボードの種類(日本語/英語/記号/絵文字)を切り替えるボタンを押した時には発火しないように作られています。ありがたいですね。
しかしこのonKeyPress
、キーボード上に表示される予測変換を選択した時にも発火するようです。
iOS(少なくともiOS17)については予測変換の単語選択の全てでonKeyPressが発火し、単語の直後に空白スペースが入る時も発火するようです。
Androidの場合は、入力中の文字に対する予測変換の選択時とその直後の空白スペースの自動入力時には発火しましたが、その後出てくる予測変換の選択では発火しませんでした。
これは認識しておいた方がよさそう。
また、この挙動があるので「エンターキーやバックスペースキーを実際に押した時」と「予測変換からEnterやBackspaceを選択した時」の区別をつけられなさそうです🤔
これまで何度か、このonKeyPress
で以下のような分岐を書いていたのですが
if (keyValue === 'Enter') {
}
意図しない発火を捕捉できていなさそうだ・・・。
これって解決方法あるんですかね?🤔
もしご存じの方がいたら教えてください!
また、日本語のローマ字入力などをする場合は、キーボードがアルファベットを他の言語へ自動で変換する時にもonKeyPress
は呼ばれるようなので注意が必要そうでした。
onBlur
,onEndEditing
,onSubmitEditing
onBlur: function
= TextInput
からフォーカスが外れた時に呼ばれる
onEndEditing: function
= TextInput
のテキスト入力が終わった時に呼ばれる
onSubmitEditing: ({nativeEvent: {text, eventCount, target}}) => void
= submit
ボタンが押された時に呼ばれる
ドキュメントを読む限り、入力が終わった時のテキストを取得したい場合はonEndEditing
かonSubmitEditing
を使うのがよいらしいです。
oBlur
でもその時のテキストを引数から取得できたのですが、推奨されていませんでした。
onSubmitEditing
は、「submitボタンを押してフォーカスが外れる時」には発火し、「他の要素をタップしてフォーカスが外れる時」は発火しないようでした。
「submitボタンを押してフォーカスが外れる時」の発火順
- onSubmitEditing
- onEndEditing
- onBlur
「他の要素をタップしてフォーカスが外れる時」の発火順
- onEndEditing
- onBlur
これら2つの動作を区別して処理を分けたい場合は、onSubmitEdiging
が呼ばれた時にコンポーネントのstateを操作し、onEndEditing
でそのstateを参照することで実現できそうです。
const [isSubmitFlow,setIsSubmitFlow] = useState(false);
const onSubmitEditing = (e) => {
setIsSubmitFlow(true);
};
const onEndEditing = (e) => {
if(!isSubmitFlow){
return;
}
};
const onBlur = (e) => {
setIsSubmitFlow(false);
};
onLayout
, onScroll
, onContentSizeChange
, onSelectionChange
onLayout: ({nativeEvent: LayoutEvent}) => void
= 要素がマウントされた時と、レイアウトに変更があった時に呼ばれます
onScroll: {nativeEvent: {contentOffset: {x, y} }}) => void
= TextInput
の中身がスクロールされた時に呼ばれます
onContentSizeChange: ({nativeEvent: {contentSize: {width, height} }}) => void
= TextInput
の中身の大きさに変更があった時に呼ばれます
onSelectionChange: ({nativeEvent: {selection: {start, end} }}) => void
= 選択中のテキスト範囲に変更があった時に呼ばれます。
ここら辺は名前の通りの挙動かなと思います。
が、onSelectionChange
についてはテキスト選択中の時のみ発火するのではなく、
カーソルの位置が変わった時にも発火するので注意が必要です。
startとendが同じ値の時は単純なカーソル位置、startとendが違う場合は選択中の範囲を示します。
そして、ローマ字入力で母音を入力する時にはonSelectionChange
が2回発火するようでした。
おわりに
実際に動かしてみると、細かい挙動で認識していないものがあったので勉強になりました。
それぞれの挙動は時間が経てば忘れてしまいそうですが、
「ちょっと注意しなければいけない挙動があるぞ」
という認識を持てたので、今後TextInput
を使って多少複雑な処理を書く時に気を付けることができそうです。
また、今までTextInput
を使う時はコンポーネントのstateとTextInput
のvalueをonChangeText
でリアルタイムに同期させる処理を深く考えずに書いていたのですが
const [value, setValue] = useState("Hello World");
const onChangeText = (text) => {
setValue(text);
}
return <TextInput value={value} onChangeText={onChangeText} />;
リアルタイムで中身を参照する必要がないのなら、onEndEditing
で入力が終わった時だけ同期するのでもいいのか?と思ったりしました。
const [value, setValue] = useState("");
const onEndEditing = (e) => {
setValue(e.nativeEvent.text);
}
return <TextInput onEndEditing={onEndEditing} defaultValue="Hello World"/>;
また時間がある時に考えてみようと思います。
この記事は
React Native Advent Calendar 2023 3日目の記事でした!