wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

クラスコンポーネントで書いていたものReact Hooksに変換してみる

github.com

↑の対応で@react-native-community/eslint-configを導入したら、

  66:5  warning  Do not use setState in componentDidMount   react/no-did-mount-set-state
  79:7  warning  Do not use setState in componentDidUpdate  react/no-did-update-set-state
  51:9  warning  Do not use setState in componentDidMount  react/no-did-mount-set-state
  60:9  warning  Do not use setState in componentDidMount  react/no-did-mount-set-state
  43:5  warning  Do not use setState in componentDidMount  react/no-did-mount-set-state

✖ 25 problems (0 errors, 25 warnings)

ライフサイクルとsetState系のwarningが出るようになったので、今はReact Hooksに対応しつつwarningを減らしていっています。

React Hooksとは

ja.reactjs.org

React 16.8 で追加された新機能でクラスコンポーネントを書かなくても値管理ができるようになります。

置き換え

元のクラスコンポーネントが以下の通りです。。

class HomeScreen extends Component<Props, State> {
  static navigationOptions = ({
    navigation,
  }: {
    navigation: NavigationScreenProp<NavigationRoute>;
  }) => {
    const { params = {} } = navigation.state;

    return {
      headerTitle: <LogoTitle />,
      headerStyle: {
        backgroundColor: theme().mode.header.backgroundColor,
      },
      headerRight: (
        <View style={styles.headerRight}>
          <Hint onPress={params.onPushCreatePlan} testID="ScheduleAdd">
            <Feather
              name="plus"
              size={28}
              color={
                darkMode()
                  ? theme().color.highLightGray
                  : theme().color.lightGreen
              }
            />
          </Hint>
        </View>
      ),
    };
  };

  state = {
    refresh: '',
    mask: false,
  };

  async componentDidMount() {
    this.props.navigation.setParams({
      onPushCreatePlan: async () => {
        this.setState({
          mask: false,
        });

        await AsyncStorage.setItem('FIRST_CRAEATE_ITEM', 'true');
        this.props.navigation.navigate('CreatePlan');
      },
    });

    const mask = await AsyncStorage.getItem('FIRST_CRAEATE_ITEM');

    this.setState({
      mask: !mask,
    });
  }

  render() {
    const refresh = this.props.navigation.getParam('refresh', '');

    return (
      <ItemsConsumer>
        {({ items, about, refreshData, itemsLoading }: ContextProps) => (
          <ThemeConsumer>
            {({ rerendering, onFinishRerendering }: ThemeContextProps) => (
              <>
                <HomeScreenPlan
                  loading={Boolean(itemsLoading)}
                  navigation={this.props.navigation}
                  rerendering={rerendering}
                  items={items}
                  about={about}
                  refresh={refresh}
                  refreshData={refreshData}
                  onFinishRerendering={onFinishRerendering}
                />
                {this.state.mask && <View style={styles.mask} />}
              </>
            )}
          </ThemeConsumer>
        )}
      </ItemsConsumer>
    );
  }
}

Contextを書き換え

まずuseContextを使用してReactのContextを使用できるようにします。

ja.reactjs.org

この部分は

return (
      <ItemsConsumer>
        {({ items, about, refreshData, itemsLoading }: ContextProps) => (
          <ThemeConsumer>
            {({ rerendering, onFinishRerendering }: ThemeContextProps) => (
                <HomeScreenPlan
                  loading={Boolean(itemsLoading)}
                  navigation={this.props.navigation}
                  rerendering={rerendering}
                  items={items}
                  about={about}
                  refresh={refresh}
                  refreshData={refreshData}
                  onFinishRerendering={onFinishRerendering}
                />
            )}
          </ThemeConsumer>
        )}
      </ItemsConsumer>
    );

こんな感じに書き換えられます

  const { items, about, refreshData, itemsLoading } = useContext(ItemsContext);
  const { rerendering, onFinishRerendering } = useContext(ThemeContext);

  return (
      <HomeScreenPlan
        loading={Boolean(itemsLoading)}
        rerendering={rerendering}
        items={items}
        about={about}
        refresh={refresh}
        refreshData={refreshData}
        onFinishRerendering={onFinishRerendering}
      />
  );

componentDidMountを書き換え

次はcomponentDidMountを書き換えます

 async componentDidMount() {
    this.props.navigation.setParams({
      onPushCreatePlan: async () => {
        this.setState({
          mask: false,
        });

        await AsyncStorage.setItem('FIRST_CRAEATE_ITEM', 'true');
        this.props.navigation.navigate('CreatePlan');
      },
    });

    const mask = await AsyncStorage.getItem('FIRST_CRAEATE_ITEM');

    this.setState({
      mask: !mask,
    });
  }

ここではuseStateuseEffectを使用します

フック API リファレンス – React

こんな感じに書き換えられます

 const [state, setState] = useState<HomeScreeState>({ mask: false });

 useEffect(() => 
    navigation.setParams({
      onPushCreatePlan: async () => {
        setState({ mask: false });

        await AsyncStorage.setItem('FIRST_CRAEATE_ITEM', 'true');
        navigation.navigate('CreatePlan');
      },
    });

    const checkMask = async () => {
      const m = await AsyncStorage.getItem('FIRST_CRAEATE_ITEM');
      setState({ mask: !m });
    };

    checkMask();
, []);

navigationOptionsの置き換え

reactnavigation.org

react-navigationのnavigationOptionsを置き換えていきます。 元はこんな感じ

class HomeScreen extends Component<Props, State> {
  static navigationOptions = ({
    navigation,
  }: {
    navigation: NavigationScreenProp<NavigationRoute>;
  }) => {
    const { params = {} } = navigation.state;

    return {
      headerTitle: <LogoTitle />,
      headerStyle: {
        backgroundColor: theme().mode.header.backgroundColor,
      },
      headerRight: (
        <View style={styles.headerRight}>
          <Hint onPress={params.onPushCreatePlan} testID="ScheduleAdd">
            <Feather
              name="plus"
              size={28}
              color={
                darkMode()
                  ? theme().color.highLightGray
                  : theme().color.lightGreen
              }
            />
          </Hint>
        </View>
      ),
    };
  };

これは、ComponentのPropertyに追加すればOK

HomeScreen.navigationOptions = ({ navigation }: NavigationOptions) => {
  const { params = {} } = navigation.state;

  return {
    headerTitle: <LogoTitle />,
    headerStyle: {
      backgroundColor: theme().mode.header.backgroundColor,
    },
    headerRight: (
      <View style={styles.headerRight}>
        <Hint onPress={params.onPushCreatePlan} testID="ScheduleAdd">
          <Feather
            name="plus"
            size={28}
            color={
              darkMode()
                ? theme().color.highLightGray
                : theme().color.lightGreen
            }
          />
        </Hint>
      </View>
    ),
  };
};

これでトップのコンポーネントの置き換えは完了です。 次にクリックなどのハンドラー関数を持つコンポーネントの置き換えについては、こちの通りです。

■ 元のコード

export class HomeScreenPlan extends Component<PlanProps, PlanState> {
(略)

  onSchedule = (id: string, title: string) => {
    this.props.navigation.navigate('Schedule', { itemId: id, title });
  };


  render() {
    return (
      <Page
        data={items}
        loading={this.props.loading}
        onSchedule={this.onSchedule}
        onDelete={this.onDelete}
      />
    );

この場合は、memouseCallbackを使用します

■ 置き換え後のコード

const HomeScreenPlan = memo((props: PlanProps) => {
(略)

  const onSchedule = useCallback(
    (id: string, title: string) => {
      navigate('Schedule', { itemId: id, title });
    },
    [navigate]
  );

  return (
    <Page
      data={items}
      loading={props.loading}
      onSchedule={onSchedule}
      onDelete={onDelete}
    />
  );
});

こうすることで、レンダリングごとにファンクションを生成し直さなくて良くなります。

pull request

この方法で1ファイルを修正したpull request

github.com