JavaScript
flowtype
react-native

flow-typed で React Navigation を使う

Flow

JavaScript は動的型付け言語ですが、Flow と合わせて使うと型情報を明示的に書いてトランスパイル時に静的な型チェックができます。殆どの JavaScript のライブラリは Flow 用の型情報を持っていませんが、メジャーなライブラリは flow-typed というツールで型情報を取得できます。詳しくは次の記事を参考にしてください。

flow-typedで外部ライブラリの型定義を使う

React Navigation と Flow

React Native で画面遷移を行うライブラリとして最近メジャーな React Navigation も Flow 用の型情報を持っていませんが、flow-typed で取得することができます。そこまではいいんですが、取得した型情報の使い方がわかりません。型情報ファイルにたくさんの型が定義されているのですが、どこでどの型を使ったらいいのかわからないのです。

とりあえずカバレッジ 100% になった

わからないなりにも試行錯誤しているうちに、どこでどれを使ったらいいかなんとなく見えてきました。そうは言っても、論理的に説明出来るほどではないのでこうすればうまく行ったという例を貼っておきます。React Navigation のチュートリアルのコードを Flow 対応させてみたものです。かなり記述量が増え、これがベストかどうかはわからないですが、同じところで困った人の参考になれば。

元のコード: https://snack.expo.io/@react-navigation/full-screen-modal

// @flow

import React from "react";
import { Button, Image, View, Text } from "react-native";
import {
  StackNavigator,
  type NavigationNavigatorProps,
  type NavigationLeafRoute,
  type StackNavigatorConfig
} from "react-navigation"; // Version can be specified in package.json

class LogoTitle extends React.Component<{}> {
  render() {
    return (
      <Image
        source={require("./spiro.png")}
        style={{ width: 30, height: 30 }}
      />
    );
  }
}

type HomeScreenNavigationParams = {
  increaseCount: () => void
};

type HomeScreenProps = NavigationNavigatorProps<
  {},
  { params: HomeScreenNavigationParams } & NavigationLeafRoute
>;

type HomeScreenState = {
  count: number
};

class HomeScreen extends React.Component<HomeScreenProps, HomeScreenState> {
  static navigationOptions = (props: HomeScreenProps) => {
    const navigation = props.navigation;
    const params = navigation.state.params || {};

    return {
      headerTitle: <LogoTitle />,
      headerLeft: (
        <Button
          onPress={() => props.navigation.navigate("MyModal")}
          title="Info"
          color="#fff"
        />
      ),
      headerRight: (
        <Button onPress={params.increaseCount} title="+1" color="#fff" />
      )
    };
  };

  componentWillMount() {
    this.props.navigation.setParams({ increaseCount: this._increaseCount });
  }

  state = {
    count: 0
  };

  _increaseCount = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <Text>Home Screen</Text>
        <Text>Count: {this.state.count}</Text>
        <Button
          title="Go to Details"
          onPress={() => {
            /* 1. Navigate to the Details route with params */
            this.props.navigation.navigate("Details", {
              itemId: 86,
              otherParam: "First Details"
            });
          }}
        />
      </View>
    );
  }
}

type DetailScreenNavigationParams = {
  itemId: number,
  otherParam: string
};

type DetailScreenProps = NavigationNavigatorProps<
  typeof mainStackNavigatorConfig.navigationOptions,
  { params: DetailScreenNavigationParams } & NavigationLeafRoute
>;

class DetailsScreen extends React.Component<DetailScreenProps> {
  static navigationOptions = (props: DetailScreenProps) => {
    const navigationOptions = props.navigationOptions;
    const { params } = props.navigation.state;

    return {
      title: params ? params.otherParam : "A Nested Details Screen",
      /* These values are used instead of the shared configuration! */
      headerStyle: {
        backgroundColor: navigationOptions
          ? navigationOptions.headerTintColor
          : null
      },
      headerTintColor: navigationOptions
        ? navigationOptions.headerStyle.backgroundColor
        : null
    };
  };

  render() {
    /* 2. Read the params from the navigation state */
    const { params } = this.props.navigation.state;
    const itemId = params ? params.itemId : null;
    const otherParam = params ? params.otherParam : null;

    return (
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <Text>Details Screen</Text>
        <Text>itemId: {JSON.stringify(itemId)}</Text>
        <Text>otherParam: {JSON.stringify(otherParam)}</Text>
        <Button
          title="Update the title"
          onPress={() =>
            this.props.navigation.setParams({ otherParam: "Updated!" })
          }
        />
        <Button
          title="Go to Details... again"
          onPress={() => this.props.navigation.navigate("Details")}
        />
        <Button
          title="Go back"
          onPress={() => this.props.navigation.goBack()}
        />
      </View>
    );
  }
}

class ModalScreen extends React.Component<
  NavigationNavigatorProps<{}, NavigationLeafRoute>
> {
  render() {
    return (
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <Text style={{ fontSize: 30 }}>This is a modal!</Text>
        <Button
          onPress={() => this.props.navigation.goBack()}
          title="Dismiss"
        />
      </View>
    );
  }
}

const mainStackNavigatorConfig = {
  initialRouteName: "Home",
  navigationOptions: {
    headerStyle: {
      backgroundColor: "#f4511e"
    },
    headerTintColor: "#fff",
    headerTitleStyle: {
      fontWeight: "bold"
    }
  }
};

const MainStack = StackNavigator(
  {
    Home: {
      screen: HomeScreen
    },
    Details: {
      screen: DetailsScreen
    }
  },
  mainStackNavigatorConfig
);

const RootStack = StackNavigator(
  {
    Main: {
      screen: MainStack
    },
    MyModal: {
      screen: ModalScreen
    }
  },
  {
    mode: "modal",
    headerMode: "none"
  }
);

export default class App extends React.Component<{}> {
  render() {
    return <RootStack />;
  }
}