独自のReact検証ライブラリの作成:基本(パート1)

公開: 2022-03-10
簡単な要約↬検証ライブラリがどのように機能するのか疑問に思ったことはありませんか? この記事では、React用の独自の検証ライブラリを段階的に構築する方法を説明します。 次のパートでは、より高度な機能をいくつか追加し、最後のパートでは、開発者エクスペリエンスの向上に焦点を当てます。

フォーム検証ライブラリはかなりクールだといつも思っていました。 私は知っています、それは持っていることはニッチな興味です—しかし、私たちはそれらをとても使います! 少なくとも私の仕事では、私が行うことのほとんどは、以前の選択とパスに依存する検証ルールを使用して、多かれ少なかれ複雑なフォームを作成することです。 フォーム検証ライブラリがどのように機能するかを理解することが最も重要です。

昨年、私はそのようなフォーム検証ライブラリを1つ作成しました。 私はそれを「Calidation」と名付けました、そしてあなたはここで紹介ブログ投稿を読むことができます。 これは、多くの柔軟性を提供し、市場に出回っている他のライブラリとは少し異なるアプローチを使用する優れたライブラリです。 しかし、他にもたくさんの素晴らしいライブラリがあります—私は私たちの要件に対してうまく機能しました。

今日は、React用の独自の検証ライブラリを作成する方法を紹介します。 プロセスを段階的に実行し、進行中にCodeSandboxの例を見つけます。 この記事の終わりまでに、独自の検証ライブラリを作成する方法を理解するか、少なくとも他のライブラリが「検証の魔法」をどのように実装するかをより深く理解することができます。

  • パート1:基本
  • パート2:機能
  • パート3:経験
ジャンプした後もっと! 以下を読み続けてください↓

ステップ1:APIを設計する

ライブラリを作成する最初のステップは、ライブラリの使用方法を設計することです。 これは、今後の多くの作業の基礎を築きます。私の意見では、これは、ライブラリで行う最も重要な決定の1つです。

「使いやすい」APIを作成することが重要ですが、将来の改善や高度なユースケースに対応できる柔軟性を備えています。 これらの両方の目標を達成しようとします。

単一の構成オブジェクトを受け入れるカスタムフックを作成します。 これにより、重大な変更を導入することなく、将来のオプションを渡すことができます。

フックに関する注記

フックはReactを書くためのかなり新しい方法です。 過去にReactを書いたことがあるなら、これらの概念のいくつかを認識していないかもしれません。 その場合は、公式ドキュメントをご覧ください。 それは信じられないほどよく書かれていて、あなたが知る必要がある基本を通してあなたを連れて行きます。

ここでは、カスタムフックuseValidationを呼び出します。 その使用法は次のようになります。

 const config = { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, onSubmit: e => { /* handle submit */ } }; const { getFieldProps, getFormProps, errors } = useValidation(config);

configオブジェクトは、各fieldsの検証ルールを設定するfieldspropを受け入れます。 さらに、フォームが送信されたときのコールバックを受け入れます。

fieldsオブジェクトには、検証する各フィールドのキーが含まれています。 各フィールドには独自の構成があり、各キーはバリデーター名であり、各値はそのバリデーターの構成プロパティです。 同じことを書く別の方法は次のようになります。

 { fields: { fieldName: { oneValidator: { validatorRule: 'validator value' }, anotherValidator: { errorMessage: 'something is not as it should' } } } }

useValidationフックは、いくつかのプロパティ( getFieldPropsgetFormProps 、およびerrors )を持つオブジェクトを返します。 最初の2つの関数は、Kent C. Doddsが「プロップゲッター」と呼んでいるものであり(これらに関する優れた記事については、こちらを参照)、特定のフォームフィールドまたはフォームタグに関連するプロップを取得するために使用されます。 errorsプロップは、フィールドごとにキー設定されたエラーメッセージを含むオブジェクトです。

この使用法は次のようになります。

 const config = { ... }; // like above const LoginForm = props => { const { getFieldProps, getFormProps, errors } = useValidation(config); return ( <form {...getFormProps()}> <label> Username<br/> <input {...getFieldProps('username')} /> {errors.username && <div className="error">{errors.username}</div>} </label> <label> Password<br/> <input {...getFieldProps('password')} /> {errors.password && <div className="error">{errors.password}</div>} </label> <button type="submit">Submit my form</button> </form> ); };

よし! そこで、APIを釘付けにしました。

  • CodeSandboxデモを見る

useValidationフックのモック実装も作成したことに注意してください。 今のところ、そこに必要なオブジェクトと関数を含むオブジェクトを返すだけなので、サンプルの実装を壊すことはありません。

フォーム状態の保存

最初に行う必要があるのは、すべてのフォームの状態をカスタムフックに保存することです。 各フィールドの値、エラーメッセージ、およびフォームが送信されたかどうかを覚えておく必要があります。 これにはuseReducerフックを使用します。これは、柔軟性が最も高い(そしてボイラープレートが少ない)ためです。 Reduxを使用したことがある場合は、おなじみの概念がいくつか表示されます。そうでない場合は、説明しながら説明します。 useReducerフックに渡されるレデューサーを作成することから始めます。

 const initialState = { values: {}, errors: {}, submitted: false, }; function validationReducer(state, action) { switch(action.type) { case 'change': const values = { ...state.values, ...action.payload }; return { ...state, values, }; case 'submit': return { ...state, submitted: true }; default: throw new Error('Unknown action type'); } }

レデューサーとは何ですか?

レデューサーは、値のオブジェクトと「アクション」を受け入れ、値オブジェクトの拡張バージョンを返す関数です。

アクションは、 typeプロパティを持つプレーンなJavaScriptオブジェクトです。 可能な各アクションタイプを処理するためにswitchステートメントを使用しています。

「値のオブジェクト」はしばしば状態と呼ばれ、私たちの場合、それは検証ロジックの状態です。

状態は、 values (フォームフィールドの現在の値)、 errors (現在のエラーメッセージのセット)、およびフォームが少なくとも1回送信されたかどうかを示すフラグisSubmittedの3つのデータで構成されます。

フォームの状態を保存するには、 useValidationフックのいくつかの部分を実装する必要があります。 getFieldPropsメソッドを呼び出すときは、そのフィールドの値、変更時の変更ハンドラー、およびどのフィールドがどれであるかを追跡するための名前propを含むオブジェクトを返す必要があります。

 function validationReducer(state, action) { // Like above } const initialState = { /* like above */ }; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); return { errors: state.errors, getFormProps: e => {}, getFieldProps: fieldName => ({ onChange: e => { if (!config.fields[fieldName]) { return; } dispatch({ type: 'change', payload: { [fieldName]: e.target.value } }); }, name: fieldName, value: state.values[fieldName], }), }; };

getFieldPropsメソッドは、各フィールドに必要な小道具を返すようになりました。 変更イベントが発生すると、フィールドが検証構成に含まれていることを確認してから、レデューサーにchangeアクションが発生したことを通知します。 レデューサーは、検証状態への変更を処理します。

  • CodeSandboxデモを見る

フォームの検証

フォーム検証ライブラリは見栄えが良いですが、フォーム値の検証に関してはあまり機能していません。 それを修正しましょう。

すべての変更イベントですべてのフィールドを検証します。 これはあまり効率的に聞こえないかもしれませんが、私が遭遇した実際のアプリケーションでは、それは実際には問題ではありません。

すべての変更ですべてのエラーを表示する必要があると言っているわけではないことに注意してください。 この記事の後半で、フィールドを送信したとき、またはフィールドから移動したときにのみエラーを表示する方法を再検討します。

バリデーター関数の選び方

バリデーターに関しては、必要なすべての検証メソッドを実装するライブラリがたくさんあります。 必要に応じて、独自に作成することもできます。 楽しい運動です!

このプロジェクトでは、先ほど書いたバリデーターのセットであるcalidatorsを使用します。 これらのバリデーターには次のAPIがあります。

 function isRequired(config) { return function(value) { if (value === '') { return config.message; } else { return null; } }; } // or the same, but terser const isRequired = config => value => value === '' ? config.message : null;

つまり、各バリデーターは構成オブジェクトを受け入れ、完全に構成されたバリデーターを返します。 その関数が値を指定して呼び出されると、値が無効な場合はmessage propを返し、有効な場合はnullを返します。 ソースコードを見ると、これらのバリデーターのいくつかがどのように実装されているかを見ることができます。

これらのバリデーターにアクセスするには、 npm install calidators installcalidatorsを使用してcalidatorsパッケージをインストールします。

単一のフィールドを検証する

useValidationオブジェクトに渡す構成を覚えていますか? 次のようになります。

 { fields: { username: { isRequired: { message: 'Please fill out a username' }, }, password: { isRequired: { message: 'Please fill out a password' }, isMinLength: { value: 6, message: 'Please make it more secure' } } }, // more stuff }

実装を簡素化するために、検証するフィールドが1つだけであると仮定します。 フィールドの構成オブジェクトの各キーを調べ、エラーが見つかるか検証が完了するまで、バリデーターを1つずつ実行します。

 import * as validators from 'calidators'; function validateField(fieldValue = '', fieldConfig) { for (let validatorName in fieldConfig) { const validatorConfig = fieldConfig[validatorName]; const validator = validators[validatorName]; const configuredValidator = validator(validatorConfig); const errorMessage = configuredValidator(fieldValue); if (errorMessage) { return errorMessage; } } return null; }

ここでは、検証する値とそのフィールドのバリデーター構成を受け入れる関数validateFieldを作成しました。 すべてのバリデーターをループし、そのバリデーターの構成を渡して実行します。 エラーメッセージが表示された場合は、残りのバリデーターをスキップして戻ります。 そうでない場合は、次のバリデーターを試します。

注:バリデーターAPIについて

APIが異なるさまざまなバリデーター(非常に人気のあるvalidator.jsなど)を選択した場合、コードのこの部分は少し異なって見える可能性があります。 ただし、簡潔にするために、その部分は読者に任せた演習とします。

注:オンfor…inループ

以前for...inありませんか? 大丈夫、これも初めてでした! 基本的に、オブジェクトのキーを繰り返し処理します。 それらについて詳しくは、MDNをご覧ください。

すべてのフィールドを検証します

1つのフィールドを検証したので、それほど問題なくすべてのフィールドを検証できるはずです。

 function validateField(fieldValue = '', fieldConfig) { // as before } function validateFields(fieldValues, fieldConfigs) { const errors = {}; for (let fieldName in fieldConfigs) { const fieldConfig = fieldConfigs[fieldName]; const fieldValue = fieldValues[fieldName]; errors[fieldName] = validateField(fieldValue, fieldConfig); } return errors; }

すべてのフィールド値とフィールド構成全体を受け入れる関数validateFieldsを作成しました。 構成内の各フィールド名をループし、そのフィールドを構成オブジェクトと値で検証します。

次へ:レデューサーに教えてください

了解しました。これで、すべてのものを検証するこの関数ができました。 それを残りのコードに取り入れましょう!

まず、 validationReducervalidateアクションハンドラーを追加します。

 function validationReducer(state, action) { switch (action.type) { case 'change': // as before case 'submit': // as before case 'validate': return { ...state, errors: action.payload }; default: throw new Error('Unknown action type'); } }

validateアクションをトリガーするたびに、状態のエラーをアクションと一緒に渡されたものに置き換えます。

次に、 useEffectフックから検証ロジックをトリガーします。

 const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };

このuseEffectフックは、最初のマウントに加えて、 state.fieldsまたはconfig.fieldsのいずれかが変更されるたびに実行されます。

バグに注意してください

上記のコードには非常に微妙なバグがあります。 useEffectフックは、 state.fieldsまたはconfig.fieldsが変更されたときにのみ再実行するように指定しました。 結局のところ、「変化」は必ずしも価値の変化を意味するわけではありません。 useEffectは、 Object.isを使用してオブジェクト間の同等性を確保し、オブジェクト間の同等性を参照します。 つまり、同じコンテンツを持つ新しいオブジェクトを渡すと、同じにはなりません(オブジェクト自体が新しいため)。

state.fieldsuseReducerから返されます。これにより、この参照の同等性が保証されますが、 configは関数コンポーネントでインラインで指定されます。 つまり、オブジェクトはレンダリングごとに再作成され、上記のuseEffectがトリガーされます。

これを解決するには、Kent C.Doddsによるuse-deep-compare-effectライブラリを使用する必要があります。 npm install use-deep-compare-effectしてインストールし、代わりにuseEffect呼び出しをこれに置き換えます。 これにより、参照の同等性チェックではなく、詳細な同等性チェックを確実に実行できます。

コードは次のようになります。

 import useDeepCompareEffect from 'use-deep-compare-effect'; const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); useDeepCompareEffect(() => { const errors = validateFields(state.fields, config.fields); dispatch({ type: 'validate', payload: errors }); }, [state.fields, config.fields]); return { // as before }; };

useEffectに関する注記

結局のところ、 useEffectは非常に興味深い関数です。 ダン・アブラモフは、このフックについてのすべてを学ぶことに興味があるなら、 useEffectの複雑さについて本当に素晴らしい長い記事を書きました。

今、物事は検証ライブラリのように見え始めています!

  • CodeSandboxデモを見る

フォーム提出の処理

基本的なフォーム検証ライブラリの最後の部分は、フォームを送信したときに何が起こるかを処理することです。 現在、ページをリロードしますが、何も起こりません。 それは最適ではありません。 フォームに関しては、デフォルトのブラウザの動作を防ぎ、代わりに自分で処理したいと考えています。 このロジックをgetFormProps関数内に配置します。

 const useValidation = config => { const [state, dispatch] = useReducer(validationReducer, initialState); // as before return { getFormProps: () => ({ onSubmit: e => { e.preventDefault(); dispatch({ type: 'submit' }); if (config.onSubmit) { config.onSubmit(state); } }, }), // as before }; };

getFormProps関数を変更して、 getFormProps関数を返します。 onSubmit関数は、DOM submitイベントがトリガーされるたびにトリガーされます。 デフォルトのブラウザの動作を防ぎ、送信したレデューサーに通知するアクションをディスパッチし、提供されたonSubmitコールバックを、提供されている場合は状態全体で呼び出します。

概要

があった! シンプルで使いやすく、非常にクールな検証ライブラリを作成しました。 ただし、インターウェブを支配する前にやるべきことはまだたくさんあります。

  • パート1:基本
  • パート2:機能
  • パート3:経験