Next.jsでMUIのStepperとReact hook formを使ってステップフォームを実装する
MUIをインストールします。
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
Material UI は、Google のMaterial Designを実装するオープンソースの React コンポーネント ライブラリです。すぐに本番環境で使用できる、事前構築済みのコンポーネントの包括的なコレクションが含まれています。マテリアル UI はデザインが美しく、独自のカスタム デザイン システムをコンポーネントの上に簡単に実装できる一連のカスタマイズ オプションを備えています。
https://mui.com/material-ui/getting-started/overview/
上記コマンドでnode moduleをインストールします。
@mui/material @mui/icons-material
はMUIのコアとなる機能を持ったnode Moduleをインストールしています。
@emotion/react @emotion/styled
はMUIの見た目を変更するスタイルエンジンであるemotionをインストールしています。
*emotionを使わずにstyled-conmponentsを使うこともできるが、SSR(server side rendaring)したいときは、styled-componentsでは動作しないようなので、不自由なければ、MUIではemotionを使いましょう。
FYI
エモーションはJavaScriptでCSSを書くときに使用するライブラリです。
MUI systemでCSSを調整する時に使うsx props
の大元で使われている技術です。
MUIのCSSの書き方 sx={{}}
を要素のタグの中で書いてスタイルを実装します。
MUI公式のスタイルエンジンについて書いているドキュメントです。
疑問
間違ったことは言ってないか?
React Hook Fromをインストールする
npm install react-hook-form
React Hook Form Performant, flexible and extensible forms with easy-to-use validation.
React Hook Formはreactでformのバリデーションを実装するための便利なライブラリです。
値の取得・監視・入力なども簡単にできるAPIがあり、日本語のドキュメントも豊富なので、reactでのフォーム実装はreact hook form一択と言っていいです(2022/12/5現在)
FYI
公式ドキュメント(今回はVersion.7を使用します。)
React Hook Formまとめ記事
ReactでFormを検討する
Stepper の実装
MUIのコンポーネントを使用します。
コード全体
import {
Box,
Button,
Container,
Step,
StepLabel,
Stepper,
Typography,
} from "@mui/material";
import { useState } from "react";
const steps = ["基本情報", "詳細情報", "確認"];
export default function Home() {
const [activeStep, setActiveStep] = useState(0);
const [skipped, setSkipped] = useState(new Set<number>());
const isStepOptional = (step: number) => {
return step === 1;
};
const isStepSkipped = (step: number) => {
return skipped.has(step);
};
const handleNext = () => {
let newSkipped = skipped;
if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped(newSkipped);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleSkip = () => {
if (!isStepOptional(activeStep)) {
// You probably want to guard against something like this,
// it should never occur unless someone's actively trying to break something.
throw new Error("You can't skip a step that isn't optional.");
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(activeStep);
return newSkipped;
});
};
const handleReset = () => {
setActiveStep(0);
};
return (
<Container>
<Box sx={{ width: "100%" }}>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps: { completed?: boolean } = {};
const labelProps: {
optional?: React.ReactNode;
} = {};
if (isStepOptional(index)) {
labelProps.optional = (
<Typography variant="caption">オプション</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === steps.length ? (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you're finished
</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Box sx={{ flex: "1 1 auto" }} />
<Button onClick={handleReset}>Reset</Button>
</Box>
</div>
) : (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Box sx={{ flex: "1 1 auto" }} />
{isStepOptional(activeStep) && (
<Button color="inherit" onClick={handleSkip} sx={{ mr: 1 }}>
Skip
</Button>
)}
<Button onClick={handleNext}>
{activeStep === steps.length - 1 ? "Finish" : "Next"}
</Button>
</Box>
</div>
)}
</Box>
</Container>
);
}
Stepper基本的な動きの理解
解説するコード
const steps = ["基本情報", "詳細情報", "確認"];
export default function Hoge() {
const [activeStep, setActiveStep] = useState(0);
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleReset = () => {
setActiveStep(0);
};
return (
<Container>
<Box sx={{ width: "100%" }}>
<Stepper activeStep={activeStep}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{activeStep === steps.length ? (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you're finished
</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Box sx={{ flex: "1 1 auto" }} />
<Button onClick={handleReset}>Reset</Button>
</Box>
</div>
) : (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Box sx={{ flex: "1 1 auto" }} />
<Button onClick={handleNext}>
{activeStep === steps.length - 1 ? "Finish" : "Next"}
</Button>
</Box>
</div>
)}
</Box>
</Container>
);
}
- Stepper要素について
<Stepper activeStep={activeStep}>
StepperタグのactiveStep属性にステップの段階を監視しているの状態変数を渡します。(今回で言うとactiveStep
です。)
現在のStepperがどの状態にあるかをactiveStep状態変数を用いて管理しています。
Stepperタグでは、コンテンツの上に表示されている「現在どのステップの段階にいる・どこまでのステップが終了しているか」、という表示を作成しています。↓
- Step要素について
const steps = ["基本情報", "詳細情報", "確認"];
~~~
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
Stepタグの要素を配列の要素の数だけ生成している。StepLabelタグでステップの番号を表し、StepLabelタグのコンテンツに書いた文字列がステップの項目名として表示される
const steps = ["基本情報", "詳細情報", "確認", "hoge1", "hog2"];
の時
- コンテンツの切り替えについて
const [activeStep, setActiveStep] = useState(0);
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleReset = () => {
setActiveStep(0);
};
~~~
{activeStep === steps.length ? (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you're finished
</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Box sx={{ flex: "1 1 auto" }} />
<Button onClick={handleReset}>Reset</Button>
</Box>
</div>
) : (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Box sx={{ flex: "1 1 auto" }} />
<Button onClick={handleNext}>
{activeStep === steps.length - 1 ? "Finish" : "Next"}
</Button>
</Box>
</div>
)}
activeSteps(ステップの状態変数)に応じて表示させるコンテンツを切り替えています
ボタン要素を押すと状態変数を切り替える関数が実行されます。
上記のような流れでステップの状態に応じてコンテンツの切り替えを行なっています。
疑問
const [activeStep, setActiveStep] = useState(0);
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
setActiveStep(activeStep + 1)
でも大丈夫なはずです。上の書き方と何が違うんですか?
この渡し方が変な感じがします。
setActiveStepで引数として渡されるものは現在のactiveStep(状態変数)であってますか?
activeStepは変化するから 動き方を固定した方がいいので、上の書き方がBetter
ステップで、項目名にオプション名を入れたい場合
上記のStepperタグのコンテンツを下記に変更する
~~~
<Stepper activeStep={activeStep}>
{/* {steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))} */}
{steps.map((label) => {
const labelProps: {
optional?: React.ReactNode;
} = {};
labelProps.optional = (
<Typography variant="caption">オプション</Typography>
);
return (
<Step key={label}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
~~~
labelPropsという名前でoptionalというプロパティを持つオブジェクトを作成します。
optionalプロパティのValueにreactエレメントを代入します。
StepLabelタグの属性に{...labelProps}
というスプレッド構文を使ったオブジェクトを渡すことによって、StepLabelタグにオプション要素を渡すことができます。
FYI
StepLabelのAPI propsの種類
スプレッド構文
疑問
{...labelProps}は
optional:( <Typography variant="caption">オプション</Typography> )
と同義ですか?
ステップのスキップ処理を実装したい
上記の「Stepper基本的な動きの理解」記事のStepperタグのコンテンツを下記に変更する。
新しい状態変数と関数を追加した。
const [skipped, setSkipped] = useState(new Set<number>());
// const handleNext = () => {
// setActiveStep((prevActiveStep) => prevActiveStep + 1);
// };
const handleNext = () => {
let newSkipped = skipped;
if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped(newSkipped);
};
const isStepSkipped = (step: number) => {
return skipped.has(step);
};
const handleSkip = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(activeStep);
return newSkipped;
});
};
~~~
<Stepper activeStep={activeStep}>
{/* {steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))} */}
{steps.map((label, index) => {
const stepProps: { completed?: boolean } = {};
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel>{label}</StepLabel>
</Step>
);
})}
</Stepper>
動き.gif
解説
スキップボタンを押すことによってlabelのチェックマークをつけずに次のステップに行ける実装を行ないました。
Stepタグの属性でcompletedというpropsがあるので、それを利用してステップの数字にチェックをつけないようにします。
状態変数が変わって画面の再レンダリングが行われたときにskippedのコレクションを確認して、値があればcompledがfalseになり、チェックマークがつかないようになります。
↓細かく見ていきます。
- Skiped状態変数について
const [skipped, setSkipped] = useState(new Set<number>());
const isStepSkipped = (step: number) => {
return skipped.has(step);
};
Setオブジェクトを持ったskippedという状態変数を作成している。(Setオブジェクトは一意の値を格納するオブジェクトである。)
- handleNext関数について
const handleNext = () => {
let newSkipped = skipped;
if (isStepSkipped(activeStep)) {
newSkipped = new Set(newSkipped.values());
newSkipped.delete(activeStep);
}
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped(newSkipped);
};
ユーザがスキップした箇所に戻ってき、フォームを入力した際に「次へ」を押すとステップの状態を完了にしたいのでSetオブジェクト.delete()でSetオブジェクトの中身を消しています。
setSkippedでSetオブジェクトの中身を更新したskippedを新しい状態変数としてセットすることで、ステップフォームの状態を更新しています。
- handleSkipについて
const handleSkip = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(activeStep);
return newSkipped;
// return prevSkipped.add(activeStep);
});
};
スキップした時に、skipped状態変数を更新し、SetオブジェクトにスキップしたStepの番号を入れて、ステップの状態を更新している。
FYI
Setオブジェクトについて
Step API
疑問
クラスは毎回newしないといけないですか?下記の書き方じゃ問題がありますか?
setSkipped((prevSkipped) => {
const newSkipped = new Set(prevSkipped.values());
newSkipped.add(activeStep);
return newSkipped;
// return prevSkipped.add(activeStep);
});
Yes クラスは毎回Newする
ステップのラベルの位置を変更したい
<Stepper activeStep={activeStep}> { /*Before*/}
↓
<Stepper activeStep={activeStep} alternativeLabel> { /*After*/}
Stepperタグの属性にalternativeLabel
を追加します。
Before
After
FYI
Stepper API
コンポーネントに分けてステップフォーム切り替えを行う
コンポーネントを作成します。
switch文を使ってステップごとに表示させるコンポーネントの切り替えを行います。(今のところ各コンポーネントは文字列を返却するだけです。)
コード全文
import {
Box,
Button,
Container,
Step,
StepLabel,
Stepper,
Typography,
} from "@mui/material";
import { useState } from "react";
import Basic from "../components/forms/Basic";
import Confirm from "../components/forms/Confirm";
import Optional from "../components/forms/Optional";
const steps = ["基本情報", "詳細情報", "確認"];
export default function Home() {
const [activeStep, setActiveStep] = useState(0);
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleReset = () => {
setActiveStep(0);
};
const changeFormConponent = (activeStep: number) => {
switch (activeStep) {
case 0:
return <Basic />;
case 1:
return <Optional />;
case 2:
return <Confirm />;
}
};
return (
<Container>
<Box sx={{ width: "100%" }}>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{activeStep === steps.length ? (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you're finished
</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Box sx={{ flex: "1 1 auto" }} />
<Button onClick={handleReset}>Reset</Button>
</Box>
</div>
) : (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
{changeFormConponent(activeStep)}
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Box sx={{ flex: "1 1 auto" }} />
<Button onClick={handleNext}>
{activeStep === steps.length - 1 ? "Finish" : "Next"}
</Button>
</Box>
</div>
)}
</Box>
</Container>
);
}
const changeFormConponent = (activeStep: number) => {
switch (activeStep) {
case 0:
return <Basic />;
case 1:
return <Optional />;
case 2:
return <Confirm />;
}
};
~~~
// リターン React Elementsの中で記入
{changeFormConponent(activeStep)}
疑問
この書き方であっていますか?他にいい方法はないですか?
関数の引数の命名が難しいので、何かいい方法はありますか?
if文でもいい 見やすい elseとかdefaultみたい
if文で書いたほう
switchMainFormとか動きがわかればいい
React Hook Formを用いてフォーム実装を行う
Basix.tsxを作成して、フォームを作っていきます。
名前入力欄を作成し、次へのボタンを押すとコンソールに入力したデータが表示されるという実装です。
コード全文
import { Box, Button, TextField } from "@mui/material";
import { Controller, useForm } from "react-hook-form";
export interface MyformBasic {
nameBox: string;
}
interface Props {
handleNext : Function
}
function Basic(props:Props) {
const { control, handleSubmit } = useForm<MyformBasic>({
defaultValues: {
nameBox: "",
},
});
const onSubmit = (data: MyformBasic) => {
console.log(data);
props.handleNext();
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="nameBox"
control={control}
render={({ field }) => (
<TextField
{...field}
type="text"
label="名前"
fullWidth
></TextField>
)}
/>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button variant="outlined" disabled sx={{ mr: 1 }}>
戻る
</Button>
<Button onClick={handleSubmit(onSubmit)} variant="outlined">
次へ
</Button>
</Box>
</form>
</div>
);
}
export default Basic;
解説
react-hook-formでのFormの作り方には2つ方法があります。
- [useForm]の
register
を使って、<input {...register('hoge')} />
とします - [useForm]の
control
と[RHFのController]を使って、<Controller ../>
とします
そもそも、Reactフォームを実装する方針としては2種類あります。
- Uncontrolled Components
- 素の
<input/>
など
- 素の
- Controlled Components
- MUIなど、外部のUI libraryが提供するfieldなど
Reactの公式ドキュメントを確認してみると、Reactでフォーム実装をする際はControlled Componentが推奨されています。
ほとんどの場合では、フォームの実装には制御されたコンポーネント (controlled component) を使用することをお勧めしています。制御されたコンポーネントでは、フォームのデータは React コンポーネントが扱います。非制御コンポーネント (uncontrolled component) はその代替となるものであり、フォームデータを DOM 自身が扱います。
https://ja.reactjs.org/docs/uncontrolled-components.html
React Hook Form(以下RHF)では、UnControlled Componentが推奨されているが、MUIなどのControlled Componentを利用するときは、Controllerコンポーネントを利用するとRHFとMUIの統合が上手にいきます。
今回の実装も、RHFのControllerコンポーネントを利用します。
React Hook Form embraces uncontrolled components and native inputs, however it's hard to avoid working with external controlled component such as React-Select, AntD and MUI. This wrapper component will make it easier for you to work with them.
React Hook Form は非制御コンポーネントとネイティブ入力を受け入れますが、React-Select、AntD、MUI などの外部制御コンポーネントの使用を避けるのは困難です。このラッパー コンポーネント(Controller)を使用すると、それらを簡単に操作できます。
https://react-hook-form.com/api/usecontroller/controller
以下細かい部分を見ていきます。↓
- useFormについて
const { control, handleSubmit } = useForm<MyformBasic>({
defaultValues: {
nameBox: "",
},
});
RHFのuseForm APIを使用して、フォームの管理を行なっています。引数にオブジェクトを渡すことでデフォルトの値などを指定できます。useFormを実行するとpropsを返却します。
Props↓
-
control RHFのControllerコンポーネントの属性 controlとuseFormのcontrol propsを使用してコンポーネントにフォームに登録する
-
handleSubmit: ((data: Object, e?: Event) => void, (errors: Object, e?: Event) => void) => Function
この関数はフォームのバリデーションが成功した時にフォームの入力データを返却します。 -
Controllerコンポーネントについて
<Controller
name="nameBox"
control={control}
render={({ field }) => (
<TextField
{...field}
type="text"
label="名前"
fullWidth
></TextField>
)}
/>
Controller コンポーネントは、RHFにおいて、Controlled Componentで使用するコンポーネントです。
Controller コンポーネントの属性を見ていきます。
- name 一意のフォームの名前 フォームの要素を取得するときにnameで指定した文字列を呼び出します
- control useFormで返却したpropsのcontrolオブジェクトを入れてこのインプット要素をuseFormに登録します
- render レンダープロップスです。reactエレメントを返却する関数です。返却するreactエレメントにRHFのイベントや値を提供して返却します。上記では、fieldオブジェクトをTextFieldタグに渡している。
FYI
非制御コンポーネントについて
コントローラーコンポーネント
レンダープロップス
registerかControllerかどっちを使うか
疑問
Controlled Uncontrolledについて理解はあっていますか?間違ったことは言ってないですか?
useFormを実行するとpropsを返却します。←ここでいうpropsっていうのは何ですか?
interface Props {
handleNext : Function
}
プロップスの型定義の仕方がわからないです
const onSubmit = (data: MyformBasic) => {
console.log(data);
props.handleNext();
};
<form onSubmit={handleSubmit(onSubmit)}>
この関数実行のやり方であってますか?
引数にdataを渡していないのに、console.logが動作するのが、なんとも言えないです。
useStateでStepフォームで値を保持する
親コンポーネントで状態を管理して、複数の子コンポーネントにpropsとして状態を渡すuseStateを使った実装を行います。(formの部分だけで使うので、useContextは使わないです。アプリケーションの広範囲でStateを使いたい場合は導入を検討します)
index.tsx全文
import {
Box,
Button,
Container,
Step,
StepLabel,
Stepper,
Typography,
} from "@mui/material";
import { useState } from "react";
import Basic from "../components/forms/Basic";
import Confirm from "../components/forms/Confirm";
import Optional from "../components/forms/Optional";
const steps = ["基本情報", "詳細情報", "確認"];
export default function Home() {
const [activeStep, setActiveStep] = useState(0);
const [formValue, setFormValue] = useState({});
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleReset = () => {
setActiveStep(0);
};
const changeFormConponent = (activeStep: number) => {
switch (activeStep) {
case 0:
return (
<Basic
handleNext={handleNext}
formValue={formValue}
setFormValue={setFormValue}
/>
);
case 1:
return (
<Optional
handleBack={handleBack}
handleNext={handleNext}
formValue={formValue}
setFormValue={setFormValue}
/>
);
case 2:
return (
<Confirm
handleBack={handleBack}
handleNext={handleNext}
formValue={formValue}
/>
);
}
};
return (
<Container>
<Box sx={{ width: "100%" }}>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{activeStep === steps.length ? (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you're finished
</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button onClick={handleReset}>Reset</Button>
</Box>
</div>
) : (
<div>
<Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
{changeFormConponent(activeStep)}
</div>
)}
</Box>
</Container>
);
}
Basic.tsx全文
import { Box, Button, TextField } from "@mui/material";
import { Controller, useForm } from "react-hook-form";
export interface MyformBasic {
nameBox: string;
}
function Basic(props: any) {
const { control, handleSubmit, setValue } = useForm<MyformBasic>({
defaultValues: {
nameBox: "",
},
});
const onSubmit = (data: MyformBasic) => {
console.log(data);
props.handleNext();
props.setFormValue({ ...props.formValue, BasicForm: data });
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="nameBox"
control={control}
render={({ field }) => (
<TextField
{...field}
type="text"
label="名前"
fullWidth
></TextField>
)}
/>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button variant="outlined" disabled sx={{ mr: 1 }}>
戻る
</Button>
<Button onClick={handleSubmit(onSubmit)} variant="outlined">
次へ
</Button>
</Box>
</form>
</div>
);
}
export default Basic;
Optional.tsx全文
import { Box, Button, TextField } from "@mui/material";
import { Controller, useForm } from "react-hook-form";
export interface MyformOptional {
optionalBox: string;
}
function Optional(props: any) {
const { control, handleSubmit, setValue } = useForm<MyformOptional>({
defaultValues: {
optionalBox: "",
},
});
const onSubmit = (data: MyformOptional) => {
console.log(data);
props.handleNext();
props.setFormValue({ ...props.formValue, OptionalForm: data });
};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="optionalBox"
control={control}
render={({ field }) => (
<TextField
{...field}
type="text"
label="備考"
fullWidth
></TextField>
)}
/>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button variant="outlined" onClick={props.handleBack} sx={{ mr: 1 }}>
戻る
</Button>
<Button
onClick={handleSubmit(onSubmit)}
variant="outlined"
sx={{ mr: 1 }}
>
確認へ
</Button>
</Box>
</form>
</div>
);
}
export default Optional;
Confirm.tsx全文
import { Box, Button, Typography } from "@mui/material";
function Confirm(props: any) {
const handleSubmit = () => {
console.log(props.formState)
props.handleNext()
}
return (
<div>
<Typography>{props.formValue.BasicForm.nameBox}</Typography>
<Typography>{props.formValue.OptionalForm.optionalBox}</Typography>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button variant="outlined" onClick={props.handleBack} sx={{ mr: 1 }}>
戻る
</Button>
<Button onClick={handleSubmit} variant="outlined">
提出
</Button>
</Box>
</div>
);
}
export default Confirm;
以下解説
const [formValue, setFormValue] = useState({});
const changeFormConponent = (activeStep: number) => {
switch (activeStep) {
case 0:
return (
<Basic
handleNext={handleNext}
formValue={formValue}
setFormValue={setFormValue}
/>
);
case 1:
return (
<Optional
handleBack={handleBack}
handleNext={handleNext}
formValue={formValue}
setFormValue={setFormValue}
/>
);
case 2:
return (
<Confirm
handleBack={handleBack}
handleNext={handleNext}
formValue={formValue}
/>
);
}
};
親コンポーネントから子コンポーネントにpropsで状態変数と状態を更新する関数を渡しています。
function Basic(props: any) {
~~~
const onSubmit = (data: MyformBasic) => {
console.log(data);
props.handleNext();
props.setFormValue({ ...props.formValue, BasicForm: data });
};
~~~
}
propsとして、子コンポーネントで状態変数を受け取っています。
setFormState関数でformStateのオブジェクトの中身を更新しています。
FYI
状態管理のReact Hooks
-
useState
props を介して親コンポーネントから子コンポーネントに情報を渡します。 -
useContext
親コンポーネントは、その下のツリー内の任意のコンポーネントが、その下のツリーの深さに関係なく、明示的に props を介して情報を渡すことなく、一部の情報を利用できるようになります。
useContext
useContextとRedux
valueの持たせ方
疑問
Contextはいつ使うのか。
仮説:アプリケーション全体で値(State)を保持したい時
認証とかアプリケーション全体で使う時しかほとんどない
基本的にuseStateで行えるときは行う。
Formが戻ったときにステップフォームの値を保持する
React Hook FormのSetValueを使用して、formの状態変数に既存のフォームの値があれば、代入するようにする。
以下解説
const { control, handleSubmit, setValue } = useForm<MyformBasic>({
defaultValues: {
nameBox: "",
},
});
useEffect(() => {
if (props.formValue.BasicForm) {
setValue("nameBox", props.formValue.BasicForm.nameBox, {
shouldDirty: true,
});
}
}, []);
useFormのプロパティでsetValue関数を呼び出します。
useEffectを使用して、画面の再レンダリング後に一回だけsetValue関数を実行して、既存のフォームの値を代入しています。
FYI
Formにバリデーションを追加します。
Yupを使用して、React Hook Formのバリデーションを行う。挙動としては、フォームのインプット要素にカーソルをおいて離すとバリデーションが走る。バリデーションエラーが起きているときは、サブミットできないようになっている
Basic.tsxのコード
import { Box, Button, TextField } from "@mui/material";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
export interface MyformBasic {
nameBox: string;
}
const schema = yup
.object({
nameBox: yup.string().required("必須項目です"),
})
.required();
function Basic(props: any) {
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<MyformBasic>({
defaultValues: {
nameBox: "",
},
resolver: yupResolver(schema),
mode: "all",
});
const onSubmit = (data: MyformBasic) => {
console.log(data);
props.handleNext();
props.setFormValue({ ...props.formValue, BasicForm: data });
};
useEffect(() => {
if (props.formValue.BasicForm) {
setValue("nameBox", props.formValue.BasicForm.nameBox, {
shouldDirty: true,
});
}
}, []);
console.log(yupResolver(schema));
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="nameBox"
control={control}
render={({ field }) => (
<TextField
{...field}
type="text"
label="名前"
error={errors.nameBox ? true : false}
helperText={errors.nameBox?.message}
fullWidth
></TextField>
)}
/>
<Box sx={{ display: "flex", flexDirection: "row", pt: 2 }}>
<Button variant="outlined" disabled sx={{ mr: 1 }}>
戻る
</Button>
<Button onClick={handleSubmit(onSubmit)} variant="outlined">
次へ
</Button>
</Box>
</form>
</div>
);
}
export default Basic;
npm install @hookform/resolvers yup
まずyupをインストールします。
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
const schema = yup
.object({
nameBox: yup.string().required("必須項目です"),
})
.required();
function Basic(props: any) {
~~~
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<MyformBasic>({
defaultValues: {
nameBox: "",
},
resolver: yupResolver(schema),
mode: "all",
});
~~~
}
yupをインポートしてきて、schemaを定義します。schemaはフォームのバリデーションルールを持ったオブジェクトです。
useFormのresolverプロパティに渡すことで、React Hook Formとyupを繋ぎます
modeプロパティをall
にすることによって
バリデーションがかかるタイミングをサブミット時→フォーカスを当てて離した瞬間とフォームの値を入力している時になります
~~~
<Controller
name="nameBox"
control={control}
render={({ field }) => (
<TextField
{...field}
type="text"
label="名前"
error={errors.nameBox ? true : false}
helperText={errors.nameBox?.message}
fullWidth
></TextField>
)}
/>
~~~
上でuseFormから返却されたformStateオブジェクトのerrorsプロパティを呼び出して使用します。
TextFieldタグの属性errorはtrueの時TextFieldをエラー表示にします。errors.nameBoxはバリデーションエラーの時にオブジェクトが生成されるため、errors.nameBoxが生成されたタイミングでエラー表示を行うという実装を行いました。
FYI
MUIのTextField API
yupのshapeの意味
yupを使うメリットはバリデーションルールをフォームとは切り離せるところにある?
高い拡張性?