#typescript タグの付いた Snippet

React Navigation

2021/04/22

インストール

yarn でパッケージをインストールする;

yarn add @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs

Expo に関連パッケージを認識させるために、expo install を実行;

expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

Stack ナビゲーションの作成

export type RootStackParamList = {
  Main: undefined;
  SignUp: undefined;
};

routes.ts を作成し、ルーティング情報を型として定義します。undefined になっているところは、ルーティングする際に id や order などの情報を追加付与する際に、その型情報を記載する箇所です。(引数がなければ undefined で構いません)

import { RootStackParamList } from './routes';

import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

import { SignUp } from './SignUp';
import { Main } from './Main';

const Stack = createStackNavigator<RootStackParamList>();

export const Navigation = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Main">
        <Stack.Screen name="Main" component={Main} options={{ title: 'メイン画面' }} />
        <Stack.Screen name="SignUp" component={SignUp} options={{ title: 'メンバー登録' }} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};
  • name がルーティング名になる。大文字小文字を無視する。大文字含む名称が推奨されている
  • createStackNaviagtor に型を付ける。各コンポーネントの引数をまとめた型を作ると、name に型制約がつく
  • options で表示名などを指定できる

各 Screen の作成

import React from 'react';
import { StackNavigatStackScreenPropsionProp } from '@react-navigation/stack';

import { RootStackParamList } from '../Navigation';

export type MainProps = StackScreenProps<RootStackParamList, 'Main'>;

export const Main = ({ navigation }: MainProps) => (
  <Box w="100%" maxW="100%" h="100%" display="flex" justifyContent="center" alignItems="center">
    <VStack spacing={4} h="100%" px={4} py={8} bgColor={palette.white}>
      <Typography color={palette.brown} fontSize="xx-large">
        メイン画面
      </Typography>

      <Center>
        <Button label="メンバー登録" onPress={() => navigation.navigate('SignUp')} />
      </Center>
    </VStack>
  </Box>
);
  • RootStackParamList をもとに MainNavigationProps を作る
  • Main コンポーネントに react-navigation から navigation が渡されるので、それをもとにスタック制御をする
  • navigation.navigate(name) で対象の画面へ遷移できる

引数の授受(/users/:id のようなもの)

export type RootStackParamList = {
  Main: undefined;
  SignUp: undefined;
  UserDetail: { id: string }; // 受け渡ししたい引数を定義する
};

引数として渡したいパラメータの型定義を付与する。

<Button label="ユーザ画面" onPress={() => navigation.navigate('UserDetail', { id: '42' })} />

任意の画面で navigate を呼び出すと、引数に型がついており、正しい引数だけを渡せるようになっている。

export type UserDetailProps = StackScreenProps<RootStackParamList, 'UserDetail'>;

export const UserDetail = ({ navigation, route }: UserDetailProps) => (
  <Box w="100%" maxW="100%" h="100%" display="flex" justifyContent="center" alignItems="center">
    <VStack spacing={4} h="100%" px={4} py={8} bgColor={palette.white}>
      <VStack spacing={4}>
        <Typography color={palette.brown} fontSize="xx-large">
          {`ユーザ画面 id=${route.params.id}`}
        </Typography>
      </VStack>

      <Center>
        <Button label="メイン画面へ戻る" onPress={() => navigation.navigate('Main')} />
      </Center>
    </VStack>
  </Box>
);
  • route.params に型がついた props が渡されるので、これを利用すれば OK。
  • setParams でパラメータの更新が可能
  • initialParams を指定しておくことが可能
  • 親画面の params を書き換えることも可能(e.g.ユーザ作成してユーザが増えた)。この場合は navigate か goBack に引数を指定すれば良い。{merge: true}すると合成してくれる模様。

ヘッダのスタイリング

export const Navigation = () => {
  const theme = useContext<ThemeColors>(ThemeContext);
  const screenOptions: StackNavigationOptions = {
    headerStyle: { backgroundColor: theme.secondary },
    headerTintColor: palette.white,
    headerTitleStyle: { fontWeight: 'bold' },
  };

  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Main" screenOptions={screenOptions}>
        <Stack.Screen name="Main" component={Main} options={{ title: 'メイン画面' }} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};
  • Navigator に screenOptions を指定すると、ヘッダスタイルを変更できる
  • スタイルではなくコンポーネントを指定することも可能
  • 個別の Screen に対して screenOptions を指定することもできる

タブナビゲーションの追加

import { RouteProp } from '@react-navigation/core';
import {
  BottomTabBarOptions,
  BottomTabNavigationOptions,
  createBottomTabNavigator,
} from '@react-navigation/bottom-tabs';
import Feather from 'react-native-vector-icons/Feather';

// main tab に属する screen 一覧
export type MainTabParamList = {
  Home: undefined;
  UserDetail: { id: string };
};

// route.name と iconName をマッチさせる辞書
const tabIconNames: Record<keyof MainTabParamList, string> = {
  Home: 'home',
  UserDetail: 'user',
};

// route.name と color, size を使って Feather アイコンをレンダリング
const getScreenOptions = (route: RouteProp<MainTabParamList, keyof MainTabParamList>) =>
  ({
    tabBarIcon: ({ color, size }) => (
      <Feather name={tabIconNames[route.name]} size={size} color={color} />
    ),
  } as BottomTabNavigationOptions);

// 下タブのナビゲーションを作成
const Tab = createBottomTabNavigator<MainTabParamList>();

export const Main = () => {
  const theme = useContext<ThemeColors>(ThemeContext);

  // タブのアクティブ・インアクティブカラーを設定
  const tabBarOptions: BottomTabBarOptions = {
    activeTintColor: theme.secondary,
    inactiveTintColor: palette.gray,
  };

  return (
    <Tab.Navigator
      screenOptions={({ route }) => getScreenOptions(route)}
      tabBarOptions={tabBarOptions}
    >
      <Tab.Screen name="Home" component={Home} />
      <Tab.Screen name="UserDetail" component={UserDetail} initialParams={{ id: '42' }} />
    </Tab.Navigator>
  );
};
  • createBottomTabNavigator で下タブのナビゲーションを作成できる
  • tabBarOptions でアクティブカラーなどを設定できる
  • タブアイコンやアクティブ状態などは、専用のレンダリング関数を作って変更する
    • Tab.navigator#screenOptions へ route を引数とした関数を登録できる
    • 受け取った route と、tabBarIcon から受け取れる color, size などを使い、アイコンのレンダリングを行う

ナビゲーションのネスト

export const Navigation = () => (
  <NavigationContainer>
    <Stack.Navigator initialRouteName="Main" screenOptions={{ headerShown: false }}>
      <Stack.Screen name="Modal" component={Modal} />
      <Stack.Screen name="Main" component={Main} />
      <Stack.Screen name="Auth" component={Auth} />
    </Stack.Navigator>
  </NavigationContainer>
);
  • Root: StackNav
    • Modal
    • Auth: StackNav
      • SignIn
      • SignUp
    • Main: BottomTabNav
      • Home
      • UserDetail

こんな感じでネストしてナビゲーションを作成できる。

  • 子ナビゲーションごとにヒストリが作成される
  • 子ナビゲーション間で情報の共有はない
  • 子ナビゲーションでハンドルできなかったナビは、親にバブリングされる
  • 親ナビゲーションの情報は、子ナビゲーションには伝達されない
  • 親子それぞれにナビゲーション UI があった場合は、重複してレンダリングされる

ネストしたスクリーンへ props を渡したい場合は、screen, params を指定すれば良い。さらにネストした指示もできる。

navigation.navigate('Root', {
  screen: 'Settings',
  params: { user: 'jane' },
});

references

React Native のフォーム処理(react-hook-form + yup)

2021/04/08

react-hook-form の特徴

パフォーマンスにフォーカスしており、redux-form や formik より高速に動作することを謳っている。その方法論として「非同期コントロール」であることを打ち出している(redux-form や formik は同期型)。

値の保持については DOM 側にまかせ、その変更を addEventListener を介して検知するという手法をとっている模様。同期型だと値の変更ごとに書き換え(=再レンダリング)が必要だったのに対し、非同期型の場合は値の変更そのものは DOM 処理のため、React による再レンダリングは発生しない。これによりマウント後の高速性を担保している。

インストール

yarn でパッケージをインストールする;

yarn add react-hook-form yup @hookform/resolvers

ベースになるテキスト入力コンポーネントの作成

import React, { ReactNode } from 'react';
import { TextInput as RnTextInput } from 'react-native';

import { Typography } from '../../atoms/Typography';

export type TextInputProps = TextInputStyledProps & {
  label?: ReactNode;
  value?: any;
  onBlur?: () => void;
  onChangeText?: (text: string) => void;
};

export const TextInput = ({ label, ...props }: TextInputProps) => {
  return (
    <>
      {label && (
        <Typography w="100%" textAlign="left">
          {label}
        </Typography>
      )}
      <RnTextInput {...props} />
    </>
  );
};

value onBlur onChangeText などが RHF から渡されるので、それを react-native の TextInput に引き渡している。ほかに styled-components によるスタイリングも行っているが、解説の範囲外につき省略。

ベースコンポーネントを RHF の Controller でラップ

import React from 'react';
import { FieldValues, Controller, DeepMap, FieldError } from 'react-hook-form';

import { TextInput, TextInputProps } from './TextInput';
import { Typography } from '../../atoms/Typography';

import { RhfProps } from '../type';
import { palette } from '../../styles/color';

export type RhfTextInputProps<T extends FieldValues> = TextInputProps & RhfProps<T>;

export const RhfTextInput = <T extends FieldValues>({
  control,
  name,
  rules,
  defaultValue,
  ...styles
}: RhfTextInputProps<T>) => {
  return (
    <Controller
      control={control}
      name={name}
      rules={rules}
      defaultValue={defaultValue}
      render={({ field: { onChange, onBlur, value }, formState: { errors } }) => (
        <>
          <TextInput
            {...styles}
            value={value}
            onBlur={onBlur}
            onChangeText={(value) => onChange(value)}
          />
          {errors[name] && (
            <Typography w="100%" textAlign="left" color={palette.red}>
              {(errors[name] as DeepMap<FieldValues, FieldError>)?.message}
            </Typography>
          )}
        </>
      )}
    />
  );
};

RHF では React の ref を使って DOM を監視しているが、React Native の場合はこの手法が使えない。かわりに Controller というラッパーコンポーネントを提供しており、これが値の同期を行ってくれる。RN で RHF を使う場合は、基本的に Controller で囲うことが前提になりそうだ。

Controller は render props によって対象のフィールドへ付与してほしい value onBlur onChangeText などを渡してくるので、これを先程作ったベースコンポーネントに渡す。

バリデーションエラーは formState.errors[name].message に入っているので、これが存在する場合はエラー内容を表示するよう、コンポーネントの振る舞いを追加する。

Form を作成

import React from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

import { VStack } from './atoms/VStack';
import { Button } from './atoms/Button';
import { RhfTextInput } from './forms/TextInput/ReactNative';

type FormData = {
  username: string;
  password: string;
};

const schema = yup.object().shape({
  username: yup.string().required(),
  password: yup.string().required(),
});

export const FormSample = () => {
  const { control, handleSubmit } = useForm<FormData>({ resolver: yupResolver(schema) });
  const onSubmit = (data: FormData) => console.log(data);

  return (
    <VStack spacing={4} w="100%">
      <RhfTextInput control={control} name="username" label="ユーザ名" />
      <RhfTextInput control={control} name="password" label="パスワード" />

      <Button label="送信" onPress={handleSubmit(onSubmit)} w="100%" h="64px" />
    </VStack>
  );
};

react-hook-form の名前が示すとおり、すべて hooks によるデータ管理がなされる。フォーム処理の基本となる情報は useForm hook を通して取得できる。

バリデーションは yup スキーマをサポートしており、@hookform/resolvers/yupyupResolver がバリデーションに関する解決を行ってくれる。これを useForm<FormData>({ resolver: yupResolver(schema)}) という形で指定すれば、RHF が適宜バリデーション処理を行ってくれる。

useForm で得られた control は各コンポーネントに渡し、handleSubmit は送信処理関数へ渡す。

references

React Native で styled-components を使う

2021/04/06

インストール

yarn でパッケージをインストールする。types が別パッケージ、かつ React Native のものはさらに別パッケージになっているので、すべてインストールする。

yarn add styled-components
yarn add -D @types/styled-components @types/styled-components-react-native

React Native のコンポーネントをスタイリングする

styled を import し、それを経由してスタイリングしたいコンポーネントを指定する。スタイリングには CSS 構文が利用できる。

import React, { ReactNode } from 'react';
import styled from 'styled-components/native';

export const VStackStyled = styled.View`
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
`;

export type VStackProps = {
  children?: ReactNode;
};

export const VStack = ({ children }: VStackProps) => <VStackStyled children={children} />;

変数を使う

props を args とした関数を使うと、props を取り出すことができる。この値を加工して CSS として有効な値に変換する

import React, { ReactNode } from 'react';
import styled from 'styled-components/native';

export type HStackProps = {
  w?: number | string;
  maxW?: number | string;
};

export const HStack = styled.View`
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  align-items: stretch;

  width: ${({ w }: HStackProps) => w ?? 'auto'};
  max-width: ${({ maxW }: HStackProps) => maxW ?? 'auto'};
`;

テーマ

ThemeProvider を使ってテーマ変数を供給する。

export const themeColors: ThemeColors = {
  primary: colorPalettes.indigo,
  secondary: colorPalettes.brown,
};

const App = () => <ThemeProvider theme={themeColors}>...</ThemeProvider>;

利用する側は useContext を使って取り出すと良い。

import React, { useContext } from 'react';
import { ThemeContext } from 'styled-components';
import styled from 'styled-components/native';

import { ThemeColors } from '../../styles/color';

type ButtonStyledProps = {
  theme: ThemeColors;
};

const ButtonContainer = styled.TouchableOpacity`
  background-color: ${({ theme }: ButtonStyledProps) => theme.primary};
`;

export const Button = ({ onPress, title }: ButtonProps) => {
  const theme = useContext<ThemeColors>(ThemeContext);

  return (
    <ButtonContainer activeOpacity={0.75} onPress={onPress} theme={theme}>
      <ButtonText fontSize="large">{title}</ButtonText>
    </ButtonContainer>
  );
};

注意点

  • styledstyled-components/native から import するが、ほかのものは styled-components から import することが多いため、どちらを参照すべきかに注意を払う
  • React Native 版の styled-components では隣接セレクタは使用できない。ほか CSS の機能を使う場合には、Web 版で実現できたことが React Native 版では実現できない場合があるかもしれないので、公式ドキュメントを随時確認する

references

Array.map で async/await を使う

2020/12/30


Writings

blogsnippetcourse

Contact

Home©︎ suzukalight