RN で Android の back ボタンを押したとき、標準ライブラリの BackAndroid を使って制御するが、一部コンポーネントのみで BackAndroid を使いたいときには
- Component が使われるときにイベント登録する
- 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 との置換で置き換えられるけど)