LoginSignup
5
4

More than 5 years have passed since last update.

ReactNative で Android の back ボタンを押したときの挙動を HOC で制御する。(テストもあるよ)

Last updated at Posted at 2017-05-10

RN で Android の back ボタンを押したとき、標準ライブラリの BackAndroid を使って制御するが、一部コンポーネントのみで BackAndroid を使いたいときには

  1. Component が使われるときにイベント登録する
  2. Component が使われないときにイベント解除する

という一連の流れを毎度書く事になって面倒くさい、ので HOC(Higher-Order Components) で実装することで

@handleBackButton
class BackButtonHandlingComponent extends Component {
  backButtonPressed():boolean {
    return doSomething();
  }
  render() {
    return <View />;
  }
}

みたいに書きたい。

なお同じ事が出来るライブラリ

があるが、シンプルに HOC で単機能のみを実装したかったので自前で実装した。

実装する

HOC については

が詳しい。今回は HOC でラップするコンポーネントの実装には backButtonPressed():boolean が実装されて欲しかったので

で解説されてるとおり、Class<Interface> & ReactClass<any> の flowtype をつけた。

interface BackButtonPressed {
  backButtonPressed(): boolean,
}

type BackButtonPressedComponent = Class<BackButtonPressed> & ReactClass<any>;

これにより、backButtonPressed():boolean が実装されてなかった場合、

 10: type BackButtonPressedComponent = Class<BackButtonPressed> & ReactClass<any>;
                                             ^^^^^^^^^^^^^^^^^
    property `backButtonPressed` of BackButtonPressed. Property not found in
 28:     const BackButtonHandlingComponent = handleBackButton(BackButton);
                                                              ^^^^^^^^^^
                                                              BackButton.
    See: __tests__/react/hoc/handleBackButton.js:28

みたいな型エラーになる。

(が残念ながら flow v0.40 では decorators での指定 @handleBackButton(ReactComponent) では型エラーにならず...)

実装コード

特に難しいことはやっておらず、mount / unmount 時に登録・解除処理を書いているだけ。また、ios か android かで切り分け、ラップするかどうかを決めている。

// @flow

import { BackAndroid, Platform } from 'react-native';
import React, { Component } from 'react';

interface BackButtonPressed {
  backButtonPressed(): boolean,
}

type BackButtonPressedComponent = Class<BackButtonPressed> & ReactClass<any>;

export default function handleBackButton(
  WrappedComponent: BackButtonPressedComponent,
): ReactClass<any> {
  class BackButtonComponent extends Component {
    componentDidMount() {
      const { backButtonPressed } = this.targetComponentRef;
      this.backButtonPressedSubscription = BackAndroid.addEventListener(
        'backPress',
        backButtonPressed,
      );
    }

    componentWillUnmount() {
      if (this.backButtonPressedSubscription) {
        this.backButtonPressedSubscription.remove();
      }
    }

    targetComponentRef: typeof WrappedComponent;

    backButtonPressedSubscription: ?{
      remove(): void,
    };

    render() {
      return (
        <WrappedComponent
          ref={(component: typeof WrappedComponent) => {
            this.targetComponentRef = component;
          }}
          {...this.props}
        />
      );
    }
  }

  return Platform.select({
    ios: WrappedComponent,
    android: BackButtonComponent,
  });
}

HOC はテストも基本的には書きやすい。

今回はちゃんとイベント登録、解除がされているかと、Android と iOS での挙動の差異のチェックをするテストを書いた。

RN のライブラリは基本 jest.mock() でモックか出来るので書きやすい。

// @flow
/* eslint react/no-multi-comp: 0 */

import { View } from 'react-native';
import React, { Component } from 'react';

import renderer from 'react-test-renderer';

import handleBackButton from '../../../src/shared/hoc/handleBackButton';

function createHOC() {
  const remove = jest.fn();
  const mockListener = jest.fn(() => ({
    remove,
  }));
  jest.mock('BackAndroid', () => ({ addEventListener: mockListener }));

  @handleBackButton class BackButton extends Component {
    backButtonPressed() {
      return !!this;
    }
    render() {
      return <View />;
    }
  }
  const component = renderer.create(<BackButton />);
  return { component, remove, mockListener };
}

describe('Platform is android', () => {
  beforeEach(() => {
    jest.mock('Platform', () => ({ select: ({ android }) => android }));
  });

  test('backButtonPressed() is defined', () => {
    const { component, remove, mockListener } = createHOC();
    component.unmount();
    expect(mockListener).toBeCalled();
    expect(remove).toBeCalled();
  });
});

describe('Platform is ios', () => {
  beforeEach(() => {
    jest.mock('Platform', () => ({ select: ({ ios }) => ios }));
  });

  test('backButtonPressed() is defined', () => {
    const { component, remove, mockListener } = createHOC();
    component.unmount();
    expect(mockListener).not.toBeCalled();
    expect(remove).not.toBeCalled();
  });
});

って、この記事書いてから気付いたけど、RN 0.44 (2017年5月時点の最新版) で BackAndroid が Deprecated になってた… (単純な BackHandler との置換で置き換えられるけど)

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4