こんにちは。
豆腐グラタンを食べたらお腹を壊したはこだてたろうです。
妻も同じものを食べたのですがケロッとしていました。
ちょっと気になったのでAIに「同じものを食べたのにお腹を壊す人と壊さない人がいるのはなぜか」と質問したところ「抵抗力の差」と言われました。
はじめに
さて、以下の記事ではバリデーションつきの入力欄やセレクトボックスのコンポーネントを作成しました。

バリデーションをする画面でよくある仕様として「入力エラーがある場合はボタンを非活性にして押せないようにする」というものがあります。
今回は、その機能を実装してみたいと思います。
実物はこちら
リポジトリはこちら
この記事を読むことで作れるもの
この記事を読むことで、以下のような画面が作れるようになります。

少しわかりにくいですが、郵便番号の入力欄で入力エラーが起きているので送信ボタンが非活性になっています(通常時は青色のボタン)。
実装
先にコード全容です。
今回新しく追加した ValidationContext
のコードをご紹介します。
import React, {createContext, ReactNode, useContext, useState} from "react";
type ValidationError = { key: string; message: string };
type ValidationErrors = Array<ValidationError>;
type ValidationContextType = {
errors: ValidationErrors;
setError: (field: string, message: string) => void;
clearError: (field: string) => void;
getError: (field: string) => ValidationError | undefined;
};
const ValidationContext = createContext<ValidationContextType>({
errors: [],
setError: () => {},
clearError: () => {},
getError: () => undefined
});
export const ValidationContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [errors, setErrors] = useState<ValidationErrors>([]);
const setError = (field: string, message: string) => {
setErrors(prev => {
const filteredErrors = prev.filter(err => err.key !== field);
const newError: ValidationError = { key: field, message: message };
const updatedErrors: ValidationErrors = [...filteredErrors, newError];
return updatedErrors;
});
};
const clearError = (field: string) => {
setErrors(prev => prev.filter(err => err.key !== field));
};
const getError = (field: string): ValidationError | undefined => {
return errors.find(err => err.key === field);
};
return (
<ValidationContext.Provider value={{ errors, setError, clearError, getError }}>
{children}
</ValidationContext.Provider>
);
};
export const useValidationContext = () => {
return useContext(ValidationContext);
};
各部分の解説
ValidationContext
入力エラー情報を ValidationContext
に格納します。
この Context には、エラー情報として { key: string; message: string }
のオブジェクト配列を格納します。
入力エラーが起きたときに、key
にはその入力欄を特定するための値を、message
には画面に表示するエラーメッセージを入れます。
多くの場合、入力欄は複数あると思うのでエラー情報も配列で持ちます。
type ValidationError = { key: string; message: string };
type ValidationErrors = Array<ValidationError>;
type ValidationContextType = {
errors: ValidationErrors;
setError: (field: string, message: string) => void;
clearError: (field: string) => void;
getError: (field: string) => ValidationError | undefined;
};
const ValidationContext = createContext<ValidationContextType>({
errors: [],
setError: () => {},
clearError: () => {},
getError: () => undefined
});
ValidationContextProvider
ValidationContext
に格納した情報にアクセスできるようにするためのプロバイダーを作成します。
export const ValidationContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [errors, setErrors] = useState<ValidationErrors>([]);
const setError = (field: string, message: string) => {
setErrors(prev => {
const filteredErrors = prev.filter(err => err.key !== field);
const newError: ValidationError = { key: field, message: message };
const updatedErrors: ValidationErrors = [...filteredErrors, newError];
return updatedErrors;
});
};
const clearError = (field: string) => {
setErrors(prev => prev.filter(err => err.key !== field));
};
const getError = (field: string): ValidationError | undefined => {
return errors.find(err => err.key === field);
};
return (
<ValidationContext.Provider value={{ errors, setError, clearError, getError }}>
{children}
</ValidationContext.Provider>
);
};
setError
は入力エラーを ValidationContext
に格納します。
すでに別の入力欄で入力エラーが発生している可能性もあるので、既存の入力エラーを保持するようにしていることに注目してください。
また、この更新処理は説明変数を使用しているので4行あります。
やろうと思えば1行で書けるのですが、読みやすさを優先するならこのように書くのも手です。
clearError
は指定された入力欄の入力エラー情報を削除します。
getError
は指定された入力欄の入力エラー情報を返します。
useValidationContext
ValidationContext
を取得する処理は、カスタムフックに抽出します。
export const useValidationContext = () => {
return useContext(ValidationContext);
};
ValidationContext に入力エラー情報を格納する
入力欄からValidationContext
に入力エラー情報を格納するよう変更します。
label
をキーにして、入力エラーを ValidationContext
に格納していることを確認してください。
なお、ラベルはキーとしてふさわしいか問題があります。
この程度の画面の規模であれば重複することはないですが、規模が大きくなれば重複する可能性が出てきそうです。
ちゃんとするなら props.name
を新しく追加するのがいいかもしれませんね。
function LabeledTextInputWithValidation(
// props 略
) {
const { setError, clearError, getError } = useValidationContext();
const validate = (value: string): string => {
if (required && value.trim() === '') {
return `必須項目です。`;
}
if (max !== undefined && value.length > max) {
return `${max}文字以内で入力してください。`;
}
if (min !== undefined && value.length < min) {
return `${min}文字以上で入力してください。`;
}
if (pattern) {
for (const { regex, message } of pattern) {
if (!regex.test(value)) {
return message;
}
}
}
return '';
};
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
onChange(event);
const errorMessage = validate(newValue);
if (errorMessage) {
setError(label, errorMessage);
} else {
clearError(label);
}
};
return (
<>
<LabeledTextInput
id={id}
label={label}
value={value}
onChange={handleChange}
placeholder={placeholder}
/>
<div style={{color: 'red', fontSize: '1rem', minHeight: '1.5rem', marginTop: '0.5rem'}}>
<ErrorMessage text={getError(label)?.message ?? ''} />
</div>
</>
)
}
export default LabeledTextInputWithValidation;
セレクトボックスの方も同様に変更します。
function LabeledSelectBoxWithValidation(
// props 略
) {
const { setError, clearError, getError } = useValidationContext();
const validate = (value: string): string => {
if (required && value.trim() === '') {
return `必須項目です。`;
}
return '';
};
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
const newValue = event.target.value;
onChange(event);
const errorMessage = validate(newValue);
if (errorMessage) {
setError(label, errorMessage);
} else {
clearError(label);
}
};
return (
<>
<LabeledSelectBox
id={id}
label={label}
value={value}
options={options}
onChange={handleChange}
/>
<div style={{color: 'red', fontSize: '1rem', minHeight: '1.5rem', marginTop: '0.5rem'}}>
<ErrorMessage text={getError(label)?.message ?? ''} />
</div>
</>
)
}
ValidationFormButton
ValidationContext
の値を取得するボタンコンポーネントを作成します。
入力エラーが起きている場合は非活性表示になるようにしています。
import React from 'react';
import Button, {ButtonProps} from "@/app/input-field/form-state/components/Button";
import {useValidationContext} from "@/app/input-field/form-state/contexts/ValidationContext";
type ValidationFormButtonProps = Omit<ButtonProps, 'disabled'>
function ValidationFormButton({ text, type, onClick }: ValidationFormButtonProps) {
const {errors} = useValidationContext()
return <Button text={text} type={type} onClick={onClick} disabled={errors.length > 0}/>
}
export default ValidationFormButton;
ValidationContextProvider でラップする
最後に、ValidationContext
にアクセスできるようにするために、ValidationContextProvider
でフォームを囲みましょう。
// import文 略
function FormState() {
// stateなど 略
return (
<main>
<div style={{marginTop: '2rem', marginLeft: '2rem'}}>
<ValidationContextProvider>
<form onSubmit={handleSubmit}>
<div>
<LabeledTextInputWithValidation
label={'名前'}
value={name}
onChange={(event: ChangeEvent<HTMLInputElement>) => setName(event.target.value)}
placeholder={'名前を入力してください'}
required
max={50}
min={2}
/>
</div>
<div>
<LabeledTextInputWithValidation
label={'名前(カナ)'}
value={nameKana}
onChange={(event: ChangeEvent<HTMLInputElement>) => setNameKana(event.target.value)}
placeholder={'名前(カナ)を入力してください'}
required
max={50}
min={2}
pattern={[
{regex: /^[ァ-ヴ]+$/, message: 'カタカナのみ使用できます'}
]}
/>
</div>
<div style={{marginTop: '1rem'}}>
<LabeledTextInputWithValidation
label={'郵便番号'}
value={postalCode}
onChange={(event: ChangeEvent<HTMLInputElement>) => setPostalCode(event.target.value)}
placeholder={'郵便番号を入力してください'}
required
max={7}
min={7}
pattern={[
{regex: /^[0-9]+$/, message: '数字のみ使用できます'}
]}
/>
</div>
<div>
<LabeledSelectBoxWithValidation
value={prefecture}
options={prefectures}
onChange={e => setPrefecture(e.target.value)}
label={'都道府県'}
required
/>
</div>
<div>
<LabeledTextInputWithValidation
label={'市区町村以降の住所'}
value={address}
onChange={(event: ChangeEvent<HTMLInputElement>) => setAddress(event.target.value)}
placeholder={'市区町村以降の住所を入力してください'}
required
max={50}
/>
</div>
<div>
<LabeledTextInputWithValidation
label={'電話番号'}
value={phoneNumber}
onChange={(event: ChangeEvent<HTMLInputElement>) => setPhoneNumber(event.target.value)}
placeholder={'電話番号を入力してください'}
required
max={11}
min={10}
pattern={[
{regex: /^[0-9]+$/, message: '数字のみ使用できます'}
]}
/>
</div>
<div style={{marginTop: '0.5rem'}}>
<ValidationFormButton type="submit" text="送信"/>
</div>
</form>
</ValidationContextProvider>
</div>
</main>
);
}
export default FormState;
おわりに
いかがだったでしょうか?
今回は、Context を使って入力エラーが起きている場合はボタンを押せなくするようにしてみました。
このやり方は、ValidationContextProvider
がトップレベルで使われているという前提でLabeledTextInputWithValidation
や ValidationFormButton
を使います。
もし ValidationContextProvider
でラップするのを忘れたら、LabeledTextInputWithValidation
や ValidationFormButton
はちゃんと動かないので注意が必要です。
このようなコンポーネント間の依存がある状態は、好き嫌いが分かれるところですね。
もっといい方法を思いついた方は、ぜひ作ってみてください。
ハコラトリでは、「できる」を増やしてワクワクする人生を送るための情報をシェアしています。
ぜひ一緒にワクワクな人生を送れるように「できる」を増やしていきましょう。
これからもReactで色々なUIを作って紹介していきますので、「こんなUIを作ってみてほしい」「こんな場合はどうすれば?」といったアイディアを募集中です!
コメントお待ちしております。
コメント