Text#onResponderTerminate

2025-06-06

Text#onResponderTerminateは、React NativeのTextコンポーネントが提供するイベントハンドラの一つで、主にタッチレスポンダシステムに関連しています。

レスポンダシステムとは?

まず、onResponderTerminateを理解するためには、React Nativeの「レスポンダシステム」について簡単に触れる必要があります。

React Nativeでは、ユーザーのタッチイベント(タップ、スワイプなど)をどのコンポーネントが処理すべきかを決定するための「レスポンダシステム」が組み込まれています。例えば、ボタンを押したときにそのボタンがイベントを受け取るのは、このシステムが機能しているからです。

コンポーネントは、onStartShouldSetResponderonMoveShouldSetResponderなどのプロパティを使って、自分がタッチイベントの「レスポンダ」になるべきかどうかをシステムに提案できます。一度レスポンダになったコンポーネントは、onResponderGrantでイベントを受け取り始め、onResponderReleaseでイベントの終了を受け取ります。

onResponderTerminateの役割

onResponderTerminateは、レスポンダになったコンポーネントが、何らかの理由でレスポンダの座を「奪われた」ときに呼び出されるイベントハンドラです。

具体的には、以下のようなシナリオで発火します。

  1. 別のコンポーネントがレスポンダになることを要求し、それが許可された場合
    例えば、あるTextコンポーネントがタッチイベントを処理中に、その上に別のScrollViewなどが現れてスクロール操作が始まった場合などです。ScrollViewがスクロールのレスポンダになることを要求し、それが許可されると、元のTextコンポーネントのレスポンダは終了し、onResponderTerminateが呼び出されます。

  2. オペレーティングシステムからの強制終了
    電話の着信や、通知センターの表示など、OSレベルでタッチイベントが横取りされるような場合にも、現在のレスポンダのonResponderTerminateが呼び出されることがあります。

このハンドラは、通常、レスポンダが予期せず中断されたときに、コンポーネントの状態をリセットしたり、適切なクリーンアップ処理を行うために使用されます。


あるTextコンポーネントがロングプレス(長押し)を検知しているとします。ユーザーが長押しを開始すると、onResponderGrantが呼び出され、コンポーネントの背景色を変更するといった視覚的なフィードバックを与えるかもしれません。

しかし、もしユーザーが長押し中に通知センターを開いたり、別のジェスチャーが優先されたりして、Textコンポーネントがレスポンダの座を失った場合、onResponderTerminateが呼び出されます。このとき、onResponderTerminate内で背景色を元の色に戻すなど、中断された状態をリセットする処理を行うことができます。



Text#onResponderTerminate自体が直接エラーメッセージとして表示されることは稀ですが、このイベントハンドラの動作に関連して、アプリケーションの挙動がおかしくなることがあります。ここでは、その一般的なシナリオとトラブルシューティングについて解説します。

onResponderTerminate が期待通りに呼び出されない(または呼び出されすぎる)

よくあるエラー/問題

  • 逆に、意図しないタイミングでonResponderTerminateが発火し、状態がリセットされてしまう。
  • タッチイベントの途中で別のコンポーネントがレスポンダになったにも関わらず、前のTextコンポーネントのUI状態がリセットされない(例えば、長押し中にボタンの色が変わったままになる)。

考えられる原因とトラブルシューティング

  • ジェスチャーハンドラの競合

    • 原因
      react-native-gesture-handlerなどの外部ライブラリを使用している場合、ネイティブジェスチャーハンドラとReact Nativeのレスポンダシステムが競合することがあります。例えば、PanGestureHandlerがタッチを横取りしてしまう場合などです。
    • トラブルシューティング
      • ジェスチャーハンドラのsimultaneousHandlersshouldCancelWhenOutsideなどのプロパティを適切に設定し、複数のジェスチャーがどのように協調動作するかを定義します。
      • シンプルなTextコンポーネントのタッチイベントであれば、まずはreact-native-gesture-handlerを使わずに、React Native標準のレスポンダシステムで実装できるか検討します。
    • 原因
      onStartShouldSetResponderonMoveShouldSetResponder のロジックが不適切で、目的のコンポーネントがレスポンダになれていない、または他のコンポーネントが不必要にレスポンダを奪っている可能性があります。
    • トラブルシューティング
      • console.log を使って、onStartShouldSetResponder, onMoveShouldSetResponder, onResponderGrant, onResponderRelease, onResponderTerminate の各ハンドラがいつ呼び出されているかを確認してください。
      • 特にonStartShouldSetResponderonMoveShouldSetResponderの返り値(true/false)を注意深く確認し、他のコンポーネントとの競合がないかをチェックします。
      • ViewコンポーネントのpointerEventsプロパティがnoneなどに設定されていないか確認してください。これが設定されていると、そのコンポーネントはタッチイベントを受け付けません。

onResponderTerminate 内での状態更新がうまくいかない

  • onResponderTerminate内でsetStateを呼び出しても、UIが更新されない、または遅れて更新される。
  • 意図しない再レンダリング

    • 原因
      onResponderTerminateの実行中に、親コンポーネントの再レンダリングなどによってTextコンポーネントが予期せず再マウントされてしまうと、状態がリセットされずに元に戻ってしまうことがあります。
    • トラブルシューティング
      • React.memouseCallback, useMemoなどを使用して、不要な再レンダリングを防ぎます。
      • keyプロップが正しく設定されているか確認し、リスト内のアイテムなどが不適切に再マウントされていないかをチェックします。
  • 非同期処理の管理

    • 原因
      onResponderTerminate内で非同期処理(例: setTimeout, fetch)を実行している場合、その処理が完了する前にコンポーネントがアンマウントされたり、別の更新が発生したりすることがあります。
    • トラブルシューティング
      • 非同期処理を行う場合は、isMountedフラグ(クラスコンポーネントの場合)やuseRefuseEffect(関数コンポーネントの場合)を使って、コンポーネントがマウントされている間だけ状態を更新するように制御します。
      • 非同期処理ではなく、単純な同期的な状態リセットであれば、onResponderTerminate内で直接setStateを呼び出して問題ないはずです。

パフォーマンスの問題

  • onResponderTerminateのロジックが重く、レスポンダの切り替わり時にUIがフリーズする、またはカクつく。
  • 重い計算処理
    • 原因
      onResponderTerminate内で複雑な計算や大量のデータ処理を行っている。
    • トラブルシューティング
      • onResponderTerminate内では、可能な限り軽量な処理(状態のリセット、簡単なUIの調整など)に留めるべきです。
      • 重い処理が必要な場合は、InteractionManager.runAfterInteractionsなどを使用して、UIスレッドのブロックを避けるようにします。
      • useNativeDriverが適用可能なアニメーションやスタイル変更であれば、可能な限りネイティブドライバを使用します。
  • 最小限の再現コード

    • 問題が発生している部分だけを切り出し、可能な限りシンプルなコードで再現を試みます。これにより、問題の原因特定が容易になります。
  • React DevToolsの利用

    • React DevToolsを使用して、コンポーネントのレンダリング回数や、onResponderTerminate呼び出し時のコンポーネントの状態変化を監視します。


例: 長押しで色が変わるボタン(レスポンダ終了時に色をリセット)

この例では、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: ユーザーに表示するメッセージを更新します。
  1. onStartShouldSetResponder

    • return true; とすることで、この Text コンポーネントがタッチイベントのレスポンダになることを常に「希望」します。これがなければ、このコンポーネントはタッチイベントを受け取ることができません。
  2. onResponderGrant

    • この Text コンポーネントがレスポンダの権限を「獲得」したときに呼び出されます。
    • setIsPressing(true) を設定し、背景色を長押し中の色に設定します。
    • メッセージを「長押し中...」に更新します。
  3. onResponderRelease

    • ユーザーが指を「離した」ときに呼び出されます。
    • setIsPressing(false) に戻し、背景色をリセットします。
    • メッセージを「長押しが完了しました」に更新し、長押しが成功した際のアクション(例: Alert.alert)を実行します。
  4. onResponderTerminationRequest

    • 他のコンポーネント(例: 親の ScrollView や他の View)がレスポンダになることを要求したときに呼び出されます。
    • return true; とすることで、この Text コンポーネントは「どうぞ、レスポンダを譲ります」と応答し、自身のレスポンダの権限を解放します。false を返すと、このコンポーネントはレスポンダの座を保持しようとします(ただし、OSが強制的に奪う場合は除く)。
  5. onResponderTerminate

    • ここが重要です。 onResponderTerminationRequesttrue を返した結果として、またはOS(例えば、iOSの通知センターを引き出すなど)によって強制的にレスポンダの権限が「奪われた」ときに呼び出されます。
    • setIsPressing(false) を設定し、背景色を元の状態にリセットします。
    • メッセージを「長押しが中断されました」に更新し、長押しが中断された際のクリーンアップ処理やUIのリセットを行います。
  1. 上記コードをReact Nativeプロジェクトで実行します。
  2. 画面中央のボックスを長押ししてください。背景色が緑に変わり、「長押し中...」と表示されます。
  3. そのまま指を離さずに、画面の別の場所を軽くタップしてみてください。または、iOSシミュレータ/実機で通知センターを上から引き下ろしてみてください。
  4. すると、Textコンポーネントはレスポンダの座を失い、onResponderTerminateが呼び出され、背景色が元の灰色に戻り、「長押しが中断されました」というメッセージが表示されます。


Pressable コンポーネントの利用

React Native 0.63 から導入された Pressable コンポーネントは、従来の TouchableWithoutFeedbackTouchableOpacity などの Touchable コンポーネント群を置き換えるもので、より豊富なプレスイベントハンドラを提供します。特に、onPressOutonResponderRelease に似た振る舞いをしますが、Pressable はより扱いやすいインターフェースを提供します。

onResponderTerminate の直接的な代替というよりは、レスポンダの終了を直接処理する必要があるケースが減り、一般的な「プレスが終了した」というイベントで十分な場合によく利用されます。

特徴

  • pressed 状態に基づいてスタイルを動的に変更できる(style プロパティに関数を渡す)。
  • pressRetentionOffset など、より細やかな制御が可能。
  • onPressIn, onPressOut, onPress, onLongPress などのイベントハンドラを提供。

onResponderTerminate の代わりとなるケース

  • もし、onResponderTerminate で行っていたクリーンアップ処理が、ユーザーが指を離した (onPressOut) ときでも問題ない場合、PressableonPressOut で代用できます。

コード例

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
    より一般的なプレスイベント(押下開始、押下終了、長押しなど)を扱うための、現代的で使いやすいコンポーネントです。多くの単純なボタンやインタラクティブな要素にはこれで十分です。onPressOutonResponderTerminateがカバーする中断の側面も一部扱えます。
  • onResponderTerminate
    React Nativeの低レベルなレスポンダシステムの一部で、特定のユースケース(他の要素によるレスポンダの奪取、OSレベルの中断など)で、コンポーネントがタッチイベントの処理を中断させられた際にクリーンアップを行うのに適しています。