Text#onResponderTerminate
Text#onResponderTerminate
は、React NativeのText
コンポーネントが提供するイベントハンドラの一つで、主にタッチレスポンダシステムに関連しています。
レスポンダシステムとは?
まず、onResponderTerminate
を理解するためには、React Nativeの「レスポンダシステム」について簡単に触れる必要があります。
React Nativeでは、ユーザーのタッチイベント(タップ、スワイプなど)をどのコンポーネントが処理すべきかを決定するための「レスポンダシステム」が組み込まれています。例えば、ボタンを押したときにそのボタンがイベントを受け取るのは、このシステムが機能しているからです。
コンポーネントは、onStartShouldSetResponder
やonMoveShouldSetResponder
などのプロパティを使って、自分がタッチイベントの「レスポンダ」になるべきかどうかをシステムに提案できます。一度レスポンダになったコンポーネントは、onResponderGrant
でイベントを受け取り始め、onResponderRelease
でイベントの終了を受け取ります。
onResponderTerminate
の役割
onResponderTerminate
は、レスポンダになったコンポーネントが、何らかの理由でレスポンダの座を「奪われた」ときに呼び出されるイベントハンドラです。
具体的には、以下のようなシナリオで発火します。
-
別のコンポーネントがレスポンダになることを要求し、それが許可された場合
例えば、あるText
コンポーネントがタッチイベントを処理中に、その上に別のScrollView
などが現れてスクロール操作が始まった場合などです。ScrollView
がスクロールのレスポンダになることを要求し、それが許可されると、元のText
コンポーネントのレスポンダは終了し、onResponderTerminate
が呼び出されます。 -
オペレーティングシステムからの強制終了
電話の着信や、通知センターの表示など、OSレベルでタッチイベントが横取りされるような場合にも、現在のレスポンダのonResponderTerminate
が呼び出されることがあります。
このハンドラは、通常、レスポンダが予期せず中断されたときに、コンポーネントの状態をリセットしたり、適切なクリーンアップ処理を行うために使用されます。
例
あるText
コンポーネントがロングプレス(長押し)を検知しているとします。ユーザーが長押しを開始すると、onResponderGrant
が呼び出され、コンポーネントの背景色を変更するといった視覚的なフィードバックを与えるかもしれません。
しかし、もしユーザーが長押し中に通知センターを開いたり、別のジェスチャーが優先されたりして、Text
コンポーネントがレスポンダの座を失った場合、onResponderTerminate
が呼び出されます。このとき、onResponderTerminate
内で背景色を元の色に戻すなど、中断された状態をリセットする処理を行うことができます。
Text#onResponderTerminate
自体が直接エラーメッセージとして表示されることは稀ですが、このイベントハンドラの動作に関連して、アプリケーションの挙動がおかしくなることがあります。ここでは、その一般的なシナリオとトラブルシューティングについて解説します。
onResponderTerminate が期待通りに呼び出されない(または呼び出されすぎる)
よくあるエラー/問題
- 逆に、意図しないタイミングで
onResponderTerminate
が発火し、状態がリセットされてしまう。 - タッチイベントの途中で別のコンポーネントがレスポンダになったにも関わらず、前の
Text
コンポーネントのUI状態がリセットされない(例えば、長押し中にボタンの色が変わったままになる)。
考えられる原因とトラブルシューティング
-
ジェスチャーハンドラの競合
- 原因
react-native-gesture-handler
などの外部ライブラリを使用している場合、ネイティブジェスチャーハンドラとReact Nativeのレスポンダシステムが競合することがあります。例えば、PanGestureHandler
がタッチを横取りしてしまう場合などです。 - トラブルシューティング
- ジェスチャーハンドラの
simultaneousHandlers
やshouldCancelWhenOutside
などのプロパティを適切に設定し、複数のジェスチャーがどのように協調動作するかを定義します。 - シンプルな
Text
コンポーネントのタッチイベントであれば、まずはreact-native-gesture-handler
を使わずに、React Native標準のレスポンダシステムで実装できるか検討します。
- ジェスチャーハンドラの
- 原因
-
- 原因
onStartShouldSetResponder
やonMoveShouldSetResponder
のロジックが不適切で、目的のコンポーネントがレスポンダになれていない、または他のコンポーネントが不必要にレスポンダを奪っている可能性があります。 - トラブルシューティング
console.log
を使って、onStartShouldSetResponder
,onMoveShouldSetResponder
,onResponderGrant
,onResponderRelease
,onResponderTerminate
の各ハンドラがいつ呼び出されているかを確認してください。- 特に
onStartShouldSetResponder
やonMoveShouldSetResponder
の返り値(true
/false
)を注意深く確認し、他のコンポーネントとの競合がないかをチェックします。 View
コンポーネントのpointerEvents
プロパティがnone
などに設定されていないか確認してください。これが設定されていると、そのコンポーネントはタッチイベントを受け付けません。
- 原因
onResponderTerminate 内での状態更新がうまくいかない
onResponderTerminate
内でsetState
を呼び出しても、UIが更新されない、または遅れて更新される。
-
意図しない再レンダリング
- 原因
onResponderTerminate
の実行中に、親コンポーネントの再レンダリングなどによってText
コンポーネントが予期せず再マウントされてしまうと、状態がリセットされずに元に戻ってしまうことがあります。 - トラブルシューティング
React.memo
やuseCallback
,useMemo
などを使用して、不要な再レンダリングを防ぎます。key
プロップが正しく設定されているか確認し、リスト内のアイテムなどが不適切に再マウントされていないかをチェックします。
- 原因
-
非同期処理の管理
- 原因
onResponderTerminate
内で非同期処理(例:setTimeout
,fetch
)を実行している場合、その処理が完了する前にコンポーネントがアンマウントされたり、別の更新が発生したりすることがあります。 - トラブルシューティング
- 非同期処理を行う場合は、
isMounted
フラグ(クラスコンポーネントの場合)やuseRef
とuseEffect
(関数コンポーネントの場合)を使って、コンポーネントがマウントされている間だけ状態を更新するように制御します。 - 非同期処理ではなく、単純な同期的な状態リセットであれば、
onResponderTerminate
内で直接setState
を呼び出して問題ないはずです。
- 非同期処理を行う場合は、
- 原因
パフォーマンスの問題
onResponderTerminate
のロジックが重く、レスポンダの切り替わり時にUIがフリーズする、またはカクつく。
- 重い計算処理
- 原因
onResponderTerminate
内で複雑な計算や大量のデータ処理を行っている。 - トラブルシューティング
onResponderTerminate
内では、可能な限り軽量な処理(状態のリセット、簡単なUIの調整など)に留めるべきです。- 重い処理が必要な場合は、
InteractionManager.runAfterInteractions
などを使用して、UIスレッドのブロックを避けるようにします。 useNativeDriver
が適用可能なアニメーションやスタイル変更であれば、可能な限りネイティブドライバを使用します。
- 原因
-
最小限の再現コード
- 問題が発生している部分だけを切り出し、可能な限りシンプルなコードで再現を試みます。これにより、問題の原因特定が容易になります。
-
React DevToolsの利用
- React DevToolsを使用して、コンポーネントのレンダリング回数や、
onResponderTerminate
呼び出し時のコンポーネントの状態変化を監視します。
- React DevToolsを使用して、コンポーネントのレンダリング回数や、
例: 長押しで色が変わるボタン(レスポンダ終了時に色をリセット)
この例では、Text
コンポーネントをボタンのように見立て、長押ししている間は背景色を緑にし、指を離すか、あるいは別のコンポーネントにレスポンダの権限を奪われた(中断された)場合に背景色を元の灰色に戻します。
import React, { useState } from 'react';
import { View, Text, StyleSheet, Alert } from 'react-native';
const PressableText = () => {
const [isPressing, setIsPressing] = useState(false);
const [message, setMessage] = useState('長押ししてください');
// レスポンダになるべきかを判断
const onStartShouldSetResponder = () => {
// 常にこのTextがレスポンダになろうと試みる
return true;
};
// レスポンダの権限が与えられたとき
const onResponderGrant = () => {
console.log('onResponderGrant: レスポンダになりました');
setIsPressing(true); // 押されている状態にする
setMessage('長押し中...');
};
// レスポンダを解放したとき(指を離したとき)
const onResponderRelease = () => {
console.log('onResponderRelease: レスポンダを解放しました');
setIsPressing(false); // 押されていない状態に戻す
setMessage('長押しが完了しました');
// 長押しが成功したとみなし、何らかのアクションを実行する
Alert.alert('アクション完了', '長押しが正常に終了しました。');
};
// 別のコンポーネントがレスポンダになることを要求したとき、
// このTextがレスポンダを解放すべきかを判断
const onResponderTerminationRequest = () => {
console.log('onResponderTerminationRequest: 他のコンポーネントがレスポンダを要求');
// ここでtrueを返すと、他のコンポーネントにレスポンダを渡すことを許可する
// (例: スクロール可能な領域の上で長押し中にスクロールが始まった場合など)
return true;
};
// レスポンダの権限が奪われたとき
const onResponderTerminate = () => {
console.log('onResponderTerminate: レスポンダが奪われました');
setIsPressing(false); // 押されていない状態に戻す(クリーンアップ)
setMessage('長押しが中断されました');
// レスポンダが奪われた際のクリーンアップ処理を行う
Alert.alert('中断', '長押しが中断されました。');
};
return (
<View style={styles.container}>
<Text
style={[
styles.pressableBox,
isPressing ? styles.pressingBox : styles.idleBox,
]}
onStartShouldSetResponder={onStartShouldSetResponder}
onResponderGrant={onResponderGrant}
onResponderRelease={onResponderRelease}
onResponderTerminationRequest={onResponderTerminationRequest}
onResponderTerminate={onResponderTerminate}
>
<Text style={styles.text}>{message}</Text>
</Text>
<Text style={styles.infoText}>
このボックスを長押し中に、画面の別の場所をタップしたり、
iOSの場合は通知センターをスワイプしたりしてみてください。
「長押しが中断されました」というメッセージが表示されます。
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
pressableBox: {
width: 200,
height: 100,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10,
borderWidth: 1,
borderColor: '#ccc',
marginBottom: 20,
},
idleBox: {
backgroundColor: '#f0f0f0',
},
pressingBox: {
backgroundColor: '#aaffaa', // 押している間は緑色
},
text: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
infoText: {
marginTop: 20,
textAlign: 'center',
color: '#666',
},
});
export default PressableText;
コード解説
-
isPressing
:Text
が現在長押しされているかどうかを追跡します。これにより、背景色を変更します。message
: ユーザーに表示するメッセージを更新します。
-
onStartShouldSetResponder
return true;
とすることで、このText
コンポーネントがタッチイベントのレスポンダになることを常に「希望」します。これがなければ、このコンポーネントはタッチイベントを受け取ることができません。
-
onResponderGrant
- この
Text
コンポーネントがレスポンダの権限を「獲得」したときに呼び出されます。 setIsPressing(true)
を設定し、背景色を長押し中の色に設定します。- メッセージを「長押し中...」に更新します。
- この
-
onResponderRelease
- ユーザーが指を「離した」ときに呼び出されます。
setIsPressing(false)
に戻し、背景色をリセットします。- メッセージを「長押しが完了しました」に更新し、長押しが成功した際のアクション(例:
Alert.alert
)を実行します。
-
onResponderTerminationRequest
- 他のコンポーネント(例: 親の
ScrollView
や他のView
)がレスポンダになることを要求したときに呼び出されます。 return true;
とすることで、このText
コンポーネントは「どうぞ、レスポンダを譲ります」と応答し、自身のレスポンダの権限を解放します。false
を返すと、このコンポーネントはレスポンダの座を保持しようとします(ただし、OSが強制的に奪う場合は除く)。
- 他のコンポーネント(例: 親の
-
onResponderTerminate
- ここが重要です。
onResponderTerminationRequest
でtrue
を返した結果として、またはOS(例えば、iOSの通知センターを引き出すなど)によって強制的にレスポンダの権限が「奪われた」ときに呼び出されます。 setIsPressing(false)
を設定し、背景色を元の状態にリセットします。- メッセージを「長押しが中断されました」に更新し、長押しが中断された際のクリーンアップ処理やUIのリセットを行います。
- ここが重要です。
- 上記コードをReact Nativeプロジェクトで実行します。
- 画面中央のボックスを長押ししてください。背景色が緑に変わり、「長押し中...」と表示されます。
- そのまま指を離さずに、画面の別の場所を軽くタップしてみてください。または、iOSシミュレータ/実機で通知センターを上から引き下ろしてみてください。
- すると、
Text
コンポーネントはレスポンダの座を失い、onResponderTerminate
が呼び出され、背景色が元の灰色に戻り、「長押しが中断されました」というメッセージが表示されます。
Pressable コンポーネントの利用
React Native 0.63 から導入された Pressable
コンポーネントは、従来の TouchableWithoutFeedback
や TouchableOpacity
などの Touchable
コンポーネント群を置き換えるもので、より豊富なプレスイベントハンドラを提供します。特に、onPressOut
は onResponderRelease
に似た振る舞いをしますが、Pressable
はより扱いやすいインターフェースを提供します。
onResponderTerminate
の直接的な代替というよりは、レスポンダの終了を直接処理する必要があるケースが減り、一般的な「プレスが終了した」というイベントで十分な場合によく利用されます。
特徴
pressed
状態に基づいてスタイルを動的に変更できる(style
プロパティに関数を渡す)。pressRetentionOffset
など、より細やかな制御が可能。onPressIn
,onPressOut
,onPress
,onLongPress
などのイベントハンドラを提供。
onResponderTerminate の代わりとなるケース
- もし、
onResponderTerminate
で行っていたクリーンアップ処理が、ユーザーが指を離した (onPressOut
) ときでも問題ない場合、Pressable
のonPressOut
で代用できます。
コード例
import React, { useState } from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';
const PressableExample = () => {
const [isPressed, setIsPressed] = useState(false);
return (
<View style={styles.container}>
<Pressable
onPressIn={() => {
console.log('Pressable: onPressIn');
setIsPressed(true);
}}
onPressOut={() => {
console.log('Pressable: onPressOut'); // 指を離した時、またはインタラクションが中断された時に近い
setIsPressed(false);
}}
onLongPress={() => {
console.log('Pressable: onLongPress');
// 長押し中のアクション
}}
style={({ pressed }) => [
styles.pressableBox,
pressed ? styles.pressingBox : styles.idleBox,
]}
>
<Text style={styles.text}>
{isPressed ? '押されています' : '押してください'}
</Text>
</Pressable>
<Text style={styles.infoText}>
Pressableは、onPressOutで指を離した時や、ジェスチャーが中断された際のクリーンアップをより簡潔に記述できます。
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
pressableBox: {
width: 200,
height: 100,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10,
borderWidth: 1,
borderColor: '#ccc',
marginBottom: 20,
},
idleBox: {
backgroundColor: '#f0f0f0',
},
pressingBox: {
backgroundColor: '#aaffaa',
},
text: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
infoText: {
marginTop: 20,
textAlign: 'center',
color: '#666',
},
});
export default PressableExample;
より複雑なジェスチャー(スワイプ、パン、ピンチ、ロングプレス、フォースタッチなど)を扱う場合、コミュニティ製の react-native-gesture-handler
ライブラリが業界標準となっています。これは、ネイティブレベルでジェスチャーを検知し、React NativeのJSスレッドへの負荷を軽減します。
onResponderTerminate
が扱うような「ジェスチャーの中断」は、このライブラリの onEnd
または onCancel
イベントでより細かく制御できます。
特徴
- 複数のジェスチャーが同時に発生した際の優先順位付けや競合解決(
simultaneousHandlers
など)が可能。 - 豊富なジェスチャータイプに対応。
- ネイティブモジュールとして実装され、パフォーマンスが高い。
onResponderTerminate の代わりとなるケース
- より堅牢で予測可能なジェスチャーのライフサイクル管理が必要な場合。
- カスタムジェスチャーを実装する場合。
- 長押し中にスワイプが開始された場合など、複雑なジェスチャー間の競合によってタッチイベントが中断される場合。
import React, { useState } from 'react';
import { View, Text, StyleSheet, Alert } from 'react-native';
import { LongPressGestureHandler, State } from 'react-native-gesture-handler';
const GestureHandlerExample = () => {
const [message, setMessage] = useState('長押ししてください');
const [boxColor, setBoxColor] = useState('#f0f0f0');
const onLongPressHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.state === State.BEGAN) {
console.log('GestureHandler: LongPress BEGAN');
setMessage('長押し中...');
setBoxColor('#aaffaa'); // 長押し開始
} else if (nativeEvent.state === State.ACTIVE) {
console.log('GestureHandler: LongPress ACTIVE');
// 長押しが継続している状態
} else if (nativeEvent.state === State.END) {
console.log('GestureHandler: LongPress END');
// 長押しが完了し、指が離された
setMessage('長押しが完了しました');
setBoxColor('#f0f0f0'); // 色をリセット
Alert.alert('アクション完了', '長押しが正常に終了しました。');
} else if (nativeEvent.state === State.CANCELLED) {
console.log('GestureHandler: LongPress CANCELLED');
// ジェスチャーが何らかの理由でキャンセルされた(onResponderTerminateに近い)
setMessage('長押しが中断されました');
setBoxColor('#f0f0f0'); // 色をリセット
Alert.alert('中断', '長押しが中断されました。');
} else if (nativeEvent.state === State.FAILED) {
console.log('GestureHandler: LongPress FAILED');
// ジェスチャーが開始される前に失敗した
setMessage('長押しが開始できませんでした');
setBoxColor('#f0f0f0');
}
};
return (
<View style={styles.container}>
<LongPressGestureHandler
onHandlerStateChange={onLongPressHandlerStateChange}
minDurationMs={500} // 500ミリ秒以上の長押しを検知
>
<View style={[styles.pressableBox, { backgroundColor: boxColor }]}>
<Text style={styles.text}>{message}</Text>
</View>
</LongPressGestureHandler>
<Text style={styles.infoText}>
react-native-gesture-handlerは、より詳細なジェスチャーの状態(BEGAN, ACTIVE, END, CANCELLEDなど)を提供し、{' '}
CANCELLEDはonResponderTerminateに似た中断イベントを扱います。
</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
pressableBox: {
width: 200,
height: 100,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10,
borderWidth: 1,
borderColor: '#ccc',
marginBottom: 20,
},
text: {
fontSize: 16,
fontWeight: 'bold',
color: '#333',
},
infoText: {
marginTop: 20,
textAlign: 'center',
color: '#666',
},
});
export default GestureHandlerExample;
- react-native-gesture-handler
複雑なジェスチャーや、高パフォーマンスが要求されるインタラクションのために、ネイティブレベルでジェスチャーを処理します。State.CANCELLED
のような詳細な状態遷移を提供し、onResponderTerminate
のような中断イベントをより堅牢に扱えます。 - Pressable
より一般的なプレスイベント(押下開始、押下終了、長押しなど)を扱うための、現代的で使いやすいコンポーネントです。多くの単純なボタンやインタラクティブな要素にはこれで十分です。onPressOut
はonResponderTerminate
がカバーする中断の側面も一部扱えます。 - onResponderTerminate
React Nativeの低レベルなレスポンダシステムの一部で、特定のユースケース(他の要素によるレスポンダの奪取、OSレベルの中断など)で、コンポーネントがタッチイベントの処理を中断させられた際にクリーンアップを行うのに適しています。