#react-native タグの付いた Snippet

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

React Native と Web/Android/iOS Simulator をセットアップする

2021/04/05

手順は下記の通り;

  • expo-cli をインストール
  • サンプルプロジェクトを自動生成
  • web で動作確認
  • iOS(Xcode/Simulator) で動作確認
  • Android(Android Studio/AVD) で動作確認

expo-cli をインストール

yarn global add expo-cli で Expo CLI をインストールできる

$ yarn global add expo-cli
success Installed "expo-cli@4.3.4" with binaries:
      - expo
      - expo-cli
✨  Done in 26.43s.

サンプルプロジェクト (AwesomeProject) を作成

expo init でプロジェクトを作成でき、yarn start で開発環境を起動できる。

$ expo init AwesomeProject
✔ Choose a template: › blank (TypeScript)    same as blank but with TypeScript configuration
✔ Downloaded and extracted project files.
🧶 Using Yarn to install packages. Pass --npm to use npm instead.
✔ Installed JavaScript dependencies.

✅ Your project is ready!

web ブラウザをターゲットとして動作確認

yarn start した CLI 上で、アプリを実行するターゲットを指定できる。シミュレータがインストールされていない環境でも、web(ブラウザ)をターゲットとして起動することができる。

$ yarn start
 › Press w │ open web

iOS (Xcode) のセットアップと動作確認

  • ‎「Xcode」を Mac App Store で のリンクをクリックして、AppStore を開き、そこから Xcode をインストール
  • $ sudo xcode-select -s /Applications/Xcode.app を実行しておく
  • Simulator を起動し、iPhone のシミュレータを表示
  • expo start したターミナルで i を入力すると、Simulator との自動連携が始まり、最終的にアプリが起動する

Android (Android Studio) のセットアップと動作確認

  • Download Android Studio and SDK tools のリンクをクリックし、Android Studio をインストール
  • ウィザードに従って初期設定を行っておく
  • AVD Manager を開き、Virtual Device を 1 つ起動しておく(Pixel など)
  • expo start したターミナルで a を入力すると、AVD との自動連携が始まり、最終的にアプリが起動する

web/iOS/Android で起動した例

/contents/snippet/2021-04-05-setup-expo-react-native/simulators.png

references

Writings

blogsnippetcourse

Contact

Home©︎ suzukalight