https://zenn.dev/highgrenade/articles/68a6fb8f907aa8
最近、プロジェクトで個人的に採用しているファイルの分け方について共有します。 これは、AtomicDesignの様な全体構成の話ではなく、Reactコンポーネント単体を見た時に、ディレクトリ内でどの様にファイルを分けているかについての話になります。
前提として
1画面が少数フォームとボタンで構成されるシンプルなもの(下記のようなレベル)

対象とするReactコンポーネントは、AtomicDesignでいうOrganismsレベルのもの
採用している技術スタックは、CSSはemotion, グローバルデータ管理はrecoil, フォーム制御にreact-hook-formとyup
紹介したいファイル構成は以下です。
Component/
|- index.tsx
|- logic.ts
|- localConstant.ts
|- errorScheme.ts
以下にそれぞれのファイルの役割についての説明を記載しています。
import { css } from '@emotion/core';
import { Input } from '@lancers/design_guideline'; // Inputフォーム
import { FormLayout } from 'component/layout'; // 送信ボタンを含むレイアウトコンポーネント
import { TitleDescriptionHorizontal } from 'component/ui'; // タイトル、詳細を表示するコンポーネント
import { mt8Css, mt40Css } from 'helper';
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={mt8Css}
name={nicknameRegister.name}
inputRef={nicknameRegister.ref}
onChange={nicknameRegister.onChange}
onBlur={nicknameRegister.onBlur}
errorMessage={errors.nickname?.message}
/>
<TitleDescriptionHorizontal
title="パスワード"
description="半角英数字 記号 8文字以上"
css={mt40Css}
/>
<Input
placeholder="例:user_001"
type="password"
css={mt8Css}
name={passwordRegister.name}
inputRef={passwordRegister.ref}
onChange={passwordRegister.onChange}
onBlur={passwordRegister.onBlur}
errorMessage={errors.password?.message}
/>
<TitleDescriptionHorizontal
title="主な利用方法"
description="どちらを選んでも、両方ご利用いただけます"
css={mt40Css}
/>
<RadioButtons
css={mt8Css}
name="purpose"
inputRef={purposeRegister.ref}
onChange={purposeRegister.onChange}
values={[
{ value: '0', text: '仕事を発注したい' },
{ value: '1', text: 'フリーランス・副業・在宅で働きたい' },
]}
errorMessage={errors.purpose?.message}
/>
</FormLayout>
);
};
import { yupResolver } from '@hookform/resolvers/yup';
import { postFirstStepSave } from 'api';
import { FORM_NAME } from 'constant';
import {
FieldError,
FieldValues,
useForm,
UseFormHandleSubmit,
UseFormRegisterReturn,
} from 'react-hook-form';
import { errorScheme } from './errorScheme';
import { LOCAL_CONSTANT } from './localConstant';
type SubmitDataArgumentType = {
[LOCAL_CONSTANT.NICKNAME_KEY]: string;
[LOCAL_CONSTANT.PASSWORD_KEY]: string;
[LOCAL_CONSTANT.PURPOSE_KEY]: '0' | '1' | '';
};
type ReturnType = {
handleSubmit: UseFormHandleSubmit<FieldValues>;
onSubmit: (data: SubmitDataArgumentType) => Promise<void>;
nicknameRegister: UseFormRegisterReturn;
passwordRegister: UseFormRegisterReturn;
purposeRegister: UseFormRegisterReturn;
errors: {
nickname?: FieldError;
password?: FieldError;
purpose?: FieldError;
};
};
export const useLogic = (): ReturnType => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<SubmitDataArgumentType>({
mode: 'onSubmit',
criteriaMode: 'all',
shouldFocusError: false,
resolver: yupResolver(errorScheme),
});
const nicknameRegister = register(LOCAL_CONSTANT.NICKNAME_KEY);
const passwordRegister = register(LOCAL_CONSTANT.PASSWORD_KEY);
const purposeRegister = register(LOCAL_CONSTANT.PURPOSE_KEY);
const onSubmit = async (data: SubmitDataArgumentType) => {
// API処理
};
return {
handleSubmit,
onSubmit,
nicknameRegister,
passwordRegister,
purposeRegister,
errors,
};
};