【脱jotai】複数画面をまたぐフォームくらいuseStateだけで実装してみたい

# なぜ脱jotai？

https://zenn.dev/uncode_jp/articles/4d2bd36aa03fc8#usestate-(react)

![cap2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/a6fb7ded-fb6d-1b16-a731-8d1f593a4d92.png)


上記記事を読んでいて、なるほどーと感じたからです。

グローバル変数がどれだけ悪さをするかはチームやアプリの規模によっても変わると思います。
ただ、使わない練習もすべきだと感じました。
今回はjotaiやReduxなどのライブラリを使用せず、複数画面で同じstateを扱ってみます。

https://github.com/suiwave/graduation-from-jotai

# つくってみた
連続する複数の入力画面と、単一の確認画面を作成してみました。

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/6ae30f84-37cb-e869-7c27-c61cacb6e2de.gif)

良い感じですね。

# コードの紹介

別に難しいことをする必要はなく、こんな感じの実装で実現できました。
（複雑な例じゃなかったのはよくなかったかも・・・）

```src/routes/RegisterRoutes.tsx
import { Routes, Route, RouteObject, useNavigate } from 'react-router-dom';
import { useUserForm } from '@/hooks/useUserForm';
import { UserProfile } from '@/schemas/user';
import Index from '@/pages/register/Index';
import InputPersonalInfo from '@/pages/register/InputPersonalInfo';
import InputContact from '@/pages/register/InputContact';
import InputAddress from '@/pages/register/InputAddress';
import ConfirmUserInfo from '@/pages/register/ConfirmUserInfo';

// Fast refreshのため、本来はコンポーネントだけexportするようなファイル分割が必要
// 今回はわかりやすさ重視のため、コンポーネントと同じファイルでRouteObjectを定義する
// eslint-disable-next-line react-refresh/only-export-components
const RegisterRoutes = () => {
    const { data, setData } = useUserForm();
    const navigate = useNavigate();

    const handleNext = (stepData: Partial<UserProfile>, nextStep: string) => {
        setData({ ...data, ...stepData });
        navigate(nextStep, { relative: 'path' }); // 遷移先を引数で指定
    };

    const handleBack = () => {
        navigate(-1)
    };

    return (
        <Routes>
            <Route path="/" element={<Index onNext={() => navigate('step1')} />} />
            <Route
                path="step1"
                element={
                    <InputPersonalInfo
                        data={data}
                        onNext={(stepData) => handleNext(stepData, '../step2')} // 現在パスからの相対パスor絶対パスしか指定できないため
                    />
                }
            />
            <Route
                path="step2"
                element={
                    <InputContact
                        data={data}
                        onNext={(stepData) => handleNext(stepData, '../step3')}
                        onBack={handleBack}
                    />
                }
            />
            <Route
                path="step3"
                element={
                    <InputAddress
                        data={data}
                        onNext={(stepData) => handleNext(stepData, '../step4')}
                        onBack={handleBack}
                    />
                }
            />
            <Route
                path="step4"
                element={
                    <ConfirmUserInfo
                        data={data}
                        onBack={handleBack}
                    />
                }
            />
        </Routes>
    );
};

const registerRoutes: RouteObject[] = [
    {
        path: '/register/*',
        element: <RegisterRoutes />,
    }
];

export default registerRoutes;
```

Routeで指定しているコンポーネントの一例です。

```src/pages/register/InputPersonalInfo.tsx
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { personalInfoSchema, PersonalInfoFormData } from "@/schemas/user";

const InputPersonalInfo:
    React.FC<{
        data: PersonalInfoFormData;
        onNext: (data: PersonalInfoFormData) => void;
    }> = ({ data, onNext }) => {
        const { register, handleSubmit, formState: { errors } } = useForm<PersonalInfoFormData>({
            defaultValues: data,
            resolver: zodResolver(personalInfoSchema),
        });

        const onSubmit = (formData: PersonalInfoFormData) => onNext(formData);

        return (
            <form onSubmit={handleSubmit(onSubmit)}>
                <div>
                    <label>名前</label>
                    <input {...register("firstName")} />
                    {errors.firstName && <p>{errors.firstName.message}</p>}
                </div>
                <div>
                    <label>姓</label>
                    <input {...register("lastName")} />
                    {errors.lastName && <p>{errors.lastName.message}</p>}
                </div>
                <div>
                    <label>年齢</label>
                    <input type="number" {...register("age", { valueAsNumber: true })} />
                    {errors.age && <p>{errors.age.message}</p>}
                </div>
                <button type="submit">次へ</button>
            </form>
        );
    };

export default InputPersonalInfo;
```


いくつかポイントがあるので確認していきます！

**Routesコンポーネント**
![cap1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/5d80b673-cebc-d86b-fc7e-8aad882852a5.png)

ドキュメントを見ると以下の記載が確認できます。

>Renders a branch of {@link Route | `<Routes>`} that best matches the current
 location. 

現在のwindowのlocationとマッチする子Routeをレンダリングする、とあります。
つまり、Routesコンポーネントはwindow.locationでレンダリングするコンポーネントを切り替えているだけとかんがえることができます。

以下のコードと似たような感じです

```tsx
    // pathを取得
    const currentLocation = window.location.pathname

    return (
        <div>
            // pathでコンポーネントを出し分け
            {currentLocation === "1" && <Step1 data={data} onNext={handleNext} />}
            {currentLocation === "2" && <Step2 data={data} onNext={handleNext} />}
        </div>
    )
```

ということで、RoutesをURLのpathによって子コンポーネントを切り替える親コンポーネントだと考えると、```useState()``` だけでjotaiなどのライブラリ抜きにstateを複数画面で管理できそうです。

今回の実装で言うと、以下の部分です

```tsx
    // Routesをレンダリングするコンポーネントでstateを定義
    const { data, setData } = useUserForm();
    // useUserFormは以下を隠蔽してるだけ
    // export const useUserForm = () => {
    //     const [data, setData] = useState<UserProfile>({
    //         firstName: "",
    //         lastName: "",
    //         age: 0, // 初期値いれたくないけどいったんこのまま
    //         email: "",
    //         phone: "",
    //         address: "",
    //         city: "",
    //         postalCode: "",
    //     });
    // 
    //     return { data, setData }
    // }
    // Routes配下のRouteコンポーネントを子とみなして、stateや更新関数を渡す
    return (
        <Routes>
            <Route path="/" element={<Index onNext={() => navigate('step1')} />} />
            <Route
                path="step1"
                element={
                    <InputPersonalInfo
                        data={data}
                        onNext={(stepData) => handleNext(stepData, '../step2')} // 現在パスからの相対パスor絶対パスしか指定できないため
                    />
                }
            />
```
**RouteObject**
また、router側で使用してもらうため、ファイル下部で以下のようにexportしています。

```tsx
const registerRoutes: RouteObject[] = [
    {
        path: '/register/*',
        element: <RegisterRoutes />,
    }
];

export default registerRoutes;
```

App.tsx側はこんな感じ

```src/App.tsx
import { RouteObject, createBrowserRouter, RouterProvider } from 'react-router-dom';
import registerRoutes from './routes/RegisterRoutes';

const mainRoutes: RouteObject[] = [
  ...registerRoutes
];

const router = createBrowserRouter(mainRoutes);

function App() {
  return <RouterProvider router={router} />;
}

export default App;
```

**Routeコンポーネント**

子コンポーネントとして使っているコンポーネントも確認します

```schemas/user.ts
// ユーザーの名前、姓、年齢といった基本的な個人情報を含むスキーマ
export const personalInfoSchema = userSchema.pick({
    firstName: true,
    lastName: true,
    age: true,
});

export type PersonalInfoFormData = z.infer<typeof personalInfoSchema>;
```

```src/pages/register/InputPersonalInfo.tsx
import { personalInfoSchema, PersonalInfoFormData } from "@/schemas/user";

const InputPersonalInfo:
    React.FC<{
        data: PersonalInfoFormData;
        onNext: (data: PersonalInfoFormData) => void;
    }> = ({ data, onNext }) => {
```

schemaで定義した、ユーザーstateの一部だけをpropsで受け取っています。
このような設計にしたことで、このコンポーネントでどのstateが更新されるのか？が把握しやすくなっています。
（globalなstateだと、ここら辺の調査がしんどくなっていくイメージ）

以上です！
簡単なサンプルだったので脱jotaiできたか若干微妙ですが、一応できたということにします！

# さいごに
グローバルなstateは影響範囲、更新の追跡などが難しくデバッグを困難にします。

どこまでその問題を重視するかはプロジェクトやチームの規模や関係性、フェーズによって異なると思います。
そういったリスクがあるよ～という勉強ができたので、ついでにそういったリスクを避ける実装も勉強してみました。

おもろかったです

https://github.com/suiwave/graduation-from-jotai