Reactコンポーネント単体のディレクトリ内のファイルについて
この話は、AtomicDesignの様な全体構成の話ではなく、Reactコンポーネント単体のディレクトリ内のファイル構成についての話です。
前提として、
1画面がフォームとボタンで構成されるシンプルなもの
今回紹介するComponentは、AtomicDesignでいうOrganismsレベルのもの
採用している技術スタックは、CSSはemotion, グローバルデータ管理はrecoil, フォーム制御にreact-hook-formとyup
紹介したいファイル構成は以下です。
code:derectory
Component/
|- index.tsx
|- logic.ts
|- localConstant.ts
|- errorScheme.ts
index.tsx
各種import、jsx、cssの記述のみに集中
メインロジックは、logic.tsに退避し、index.tsxにはロジックをゴリゴリ書かない方針
1画面がシンプルなことやデザインシステムや別ディレクトリに配置しているコンポーネントを使用している関係上、ほぼコンポーネントを置くだけの開発体験が実現中
code:ts
import { css } from '@emotion/core';
import { Input } from '@lancers/design_guideline'; // Inputフォーム
import { FormLayout } from 'component/layout'; // 送信ボタンを含むレイアウトコンポーネント
import { TitleDescriptionHorizontal } from 'component/ui'; // タイトル、詳細を表示するコンポーネント
import React from 'react';
import { useLogic } from './logic';
export const UserPassword: React.FC = () => {
const {
handleSubmit,
onSubmit,
nicknameRegister,
passwordRegister,
purposeRegister,
errors,
} = useLogic();
return (
<FormLayout
title="ユーザー名とパスワードを設定してください"
buttonType="single"
handleSubmit={handleSubmit}
onSubmit={onSubmit}
<TitleDescriptionHorizontal
title="ユーザー名"
description="半角英数字記号 4文字以上(使用可能記号 -_ )"
/>
<Input
placeholder="例:user_001"
css={mtInputCss}
name={nicknameRegister.name}
inputRef={nicknameRegister.ref}
onChange={nicknameRegister.onChange}
onBlur={nicknameRegister.onBlur}
errorMessage={errors.nickname?.message}
/>
</FormLayout>
);
};
const mtCss = css`
margin-top: 32px;
`;
const mtInputCss = css`
margin-top: 8px;
`;
logic.ts
メインのロジックを記述するファイル
index.tsxで利用する為に必要な変数や関数をreturnするカスタムフック的な記述
1画面がシンプルなので、記述コードは、ほとんどフォーム制御やAPIとの接続部分のみ
localConstant.ts
コンポーネント内で利用する定数を管理
グローバルな単位で利用する定数は、別途一極集中ファイルで管理
↓以下のような形
code:ts
export const LOCAL_CONSTANT = {
NICKNAME_KEY: 'nickname',
PASSWORD_KEY: 'password',
PURPOSE_KEY: 'purpose',
EMAIL_HASH_KEY: 'email_hash',
NICKNAME_MIN_LENGTH: 4,
NICKNAME_MAX_LENGTH: 25,
PASSWORD_MIN_LENGTH: 8,
PASSWORD_MAX_LENGTH: 32,
} as const;
errorScheme.ts
yupで記述したバリデーションを退避したファイル
react-hook-formを利用しており、バリデーションの記述もその範囲で出来ますが、react-hook-formだとメソッド内に直接バリデーションを記述することになり、logic.tsがごちゃごちゃしてしまう為、生まれたファイル
このファイルのおかげで、logic.tsがシンプルになります
react-hook-formでバリデーションを記述する場合
各registerにルールを直接記述しないといけないので可読性が悪い
code:ts
// useFormのロジック
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SubmitDataArgumentType>({
mode: 'onSubmit',
criteriaMode: 'all',
shouldFocusError: false,
});
const nicknameRegister = register('nickname', {
required: '必須入力のエラーメッセージ',
min: { value: 4, message: '最小文字数入力のエラーメッセージ' },
max: { value: 25, message: '最大文字数入力のエラーメッセージ' },
});
const passwordRegister = register('password', {required: '必須エラー'});
const purposeRegister = register('purpose', {required: '必須エラー'});
バリデーションをyupで実装する場合
resolverに対してバリデーションスキーマを渡すだけでokなので、logic.tsの内容が汚れない
code:ts
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SubmitDataArgumentType>({
mode: 'onSubmit',
criteriaMode: 'all',
shouldFocusError: false,
resolver: yupResolver(errorScheme), // resolver
});
実際のerrorScheme.tsの内容は、以下の様な感じです。
最低限の必須チェック、最小・最大文字数チェック、正規表現チェックのみを行い、そこからあふれるAPIとの接続が必要になってくるエラーチェックは、logic.ts側で記述する形になります。
code:ts
import { ERROR_MESSAGE, REGEXP } from 'constant';
import * as yup from 'yup';
import { LOCAL_CONSTANT } from './localConstant';
export const errorScheme = yup.object().shape({
.string()
.required(ERROR_MESSAGE.REQUIRED)
.matches(
REGEXP.HALF_ALPHABET_NUMERIC_UNDERSCORE_HYPHEN,
ERROR_MESSAGE.HALF_ALPHABET_NUMERIC_UNDERSCORE_HYPHEN
)
.min(
LOCAL_CONSTANT.NICKNAME_MIN_LENGTH,
LOCAL_CONSTANT.NICKNAME_MIN_LENGTH + ERROR_MESSAGE.MIN_LENGTH
)
.max(
LOCAL_CONSTANT.NICKNAME_MAX_LENGTH,
LOCAL_CONSTANT.NICKNAME_MAX_LENGTH + ERROR_MESSAGE.MAX_LENGTH
),
.string()
.required(ERROR_MESSAGE.REQUIRED)
.matches(
REGEXP.HALF_ALPHABET_NUMERIC_SYMBOL,
ERROR_MESSAGE.HALF_ALPHABET_NUMERIC_SYMBOL
)
.min(
LOCAL_CONSTANT.PASSWORD_MIN_LENGTH,
LOCAL_CONSTANT.PASSWORD_MIN_LENGTH + ERROR_MESSAGE.MIN_LENGTH
)
.max(
LOCAL_CONSTANT.PASSWORD_MAX_LENGTH,
LOCAL_CONSTANT.PASSWORD_MAX_LENGTH + ERROR_MESSAGE.MAX_LENGTH
),
.mixed()
.nullable()
.required(ERROR_MESSAGE.REQUIRED_SELECTED),
});