MUI の Stepper で Server Actions を実行する際の注意点
Stepper
を使ってServerActions
を実行すると、正しく値を入力してもバリデーションに引っかかってしまう問題
MUIのNext.jsの安定機能になったserver actions
と MUIのStepperコンポーネントを使った際にハマったので、現時点で考えられる実装方法を紹介します。
原因
MUIのStepperでは、activeな状態ではないstepのコンポーネントはレンダリングされないため、FormData
の値として判別できないのが原因でした。
原因の確認
MUIの公式で紹介しているサンプルコードで挙動を確認したいと思います。
公式のサンプルコード
'use client'
import * as React from 'react';
import Box from '@mui/material/Box';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
const steps = ['Select campaign settings', 'Create an ad group', 'Create an ad'];
export default function Page() {
const [activeStep, setActiveStep] = React.useState(0);
const [skipped, setSkipped] = React.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 (
<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">Optional</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === steps.length ? (
<React.Fragment>
<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>
</React.Fragment>
) : (
<React.Fragment>
<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>
</React.Fragment>
)}
</Box>
);
}
activeStep=1
の場合を検証ツールで確認すると以下のようになっています。
赤枠で囲っている部分がStepperのコンテンツになっており、'Step 1'という文字列を表示するpタグをレンダリングしていることがわかると思います。
activeStep=2
の場合の検証ツールは以下になります。
赤枠で囲っている部分を見ると、先ほど'Step 1'になっていたpタグが無くなり、Step 2'という文字列を表示するpタグがレンダリングされていることが確認できます。
ServerActionsで利用するFormData型はname='hoge'
のように属性を指定すると思います。
MUIのTextFieldコンポーネントでは以下のようになると思います。
<TextField name='title' />
上のStepを切り替えた時の挙動でわかるように、 「name属性で指定したコンポーネントごとDOM操作で削除されてしまうので、 ServerActions
の実行時に非アクティブ状態のコンポーネントの値が取得ず、実行時にバリデーションに引っかかってしまう」 ということになります。
解決策
ボツになった解決策候補
❌ display: 'none'
は判別できるので、送信したい値の数だけdisplay: 'none'
を指定した送信用のコンポーネントとユーザー入力用のコンポーネントを作成(name属性のみprefixで分ける)する
- 不要にレンダリングすることになりパフォーマンスが悪い
- DOM操作で値を操作が可能なのでセキュリティ的に良くない(気がする)
⭕️ 送信時に、実行する関数内でformData.append("attribute", value);
のようにして、ServerActions
に渡すFormData型の変数に値を追加する
実装例
'use client';
const Page = () => {
// NOTE: formData に値を追加
const addAttributesToFormData = (formData: FormData) => {
formData.append("attribute1", value1);
formData.append("attribute2", value2);
};
const onSubmit = async (formData: FormData) => {
// NOTE: formData に値を追加
addAttributesToFormData(formData);
// NOTE: server actions を実行
serverAction(formData);
};
return (
<Stepper
component="form"
action={onSubmit}
// その他の props
>
<Step>
<StepContent>
... // その他のコンポーネント
</StepContent>
</Step>
... // その他の Step
</Stepper>
)
};
おわりに
私はこのような方法で実装することにしましたが、Nextの経験は浅いので、他に良い方法などありましたら、ご教授いただけると助かります。
普段は、Rails,React,AWS周りの情報を発信していますので、いいねやフォローをお願いします!
Discussion