Keresési lekérdezés érvényesítése a React Router 6-tal és Yup-pal, valamint Typescript-tel
Érvényesíted a keresési lekérdezéseket a frontend appodban? A válasz „néha”, de ha a keresési lekérdezésre az alkalmazás bemeneteként gondolsz, meg fog változni a véleményed.
Ellenőrzöd a keresési lekérdezéseket a frontend alkalmazásában? Számomra a válasz "néha", de ha a keresési lekérdezést az alkalmazás bemenetének tekintjük, a válasznak mindig igennek kell lennie.
Eddig, még amikor megtettem, teljesen rosszul csináltam. Vagy lehet, hogy nem volt teljesen rossz, mivel legalább ellenőrzés történt. Még mindig lennének módok a megoldásaim olvashatóságának és újrafelhasználhatóságának javítására.
A probléma
Lássunk egy valós példát: építsünk egy "jelszó beállítása" oldalt egy alkalmazáshoz. Az egyik szokásos folyamat az, hogy a végfelhasználó egy linkre kattint az e-mailjében, a linkben van egy kód, amellyel ellenőrizhetjük a kérés érvényességét és elküldhetjük az új jelszót. A következő példában egy "oobCode"-ot és egy "mode"-t adok hozzá az URL-hez (a Firebase jelszó visszaállítási folyamatához való integráláshoz).
A (rossz?) megoldás
Az egyik módszer a Reactben (ReactRouter 6-tal) a useLocation hook használata, amely a keresést (karakterlánc, mint például ?oobCode=123aa3&mode=resetPassword) adja, majd elemzéssel átalakítja a query-string csomaggal. (Bár a useSearchParams hookot is használhatja, ez nem egyszerű JS objektumot hoz létre, így utána még mindig szükség van némi munkára.)
const { search } = useLocation();
const query = qs.parse(search);
Tehát következő lépésként az ellenőrzés. Mit szeretnénk ellenőrizni?
Az oobCode létezik és egy karakterlánc
A mode létezik és az alábbiak egyike:
resetPassword
recoverEmail
verifyEmail
Tehát valami ilyesmi megoldaná a feladatot:
enum AuthActionMode {
RESET_PASSWORD = "resetPassword",
RECOVER_EMAIL = "recoverEmail",
VERIFY_EMAIL = "verifyEmail",
}
export function CustomAuthActionPage() {
const [hasInvalidParams, setHasInvalidParams] = useState(false);
const { search } = useLocation();
const query = qs.parse(search);
useEffect(() => {
setHasInvalidParams(false);
if (typeof query.oobCode !== "string") {
setHasInvalidParams(true);
}
if (
typeof query.mode !== "string" ||
(typeof query.mode === "string" && !Object.values(AuthActionMode).includes(query.mode))
) {
setHasInvalidParams(true);
}
}, [query]);
if (hasInvalidParams) {
return <div>Érvénytelen paraméterek</div>;
}
return query.mode; // tegyen valamit a paraméterekkel
}
Elég ronda, ugye? Nem is beszélve arról, hogy ronda, de emellett a TypeScript nem nyújt "típus hint"-et sem, bár elvégzi a feladatot, és biztosak lehetünk benne, hogy a lekérdezésünk a megfelelő formátumban van.
A jobb megoldás
Akkor hogyan javíthatunk rajta? Sémavizsgálat a megmentőnk, sok nagyszerű JavaScript-alapú (és TypeScript-alapú) sémavizsgálati könyvtár található, mint például a Joi, a Yup és a Zod (és biztos vagyok benne, hogy a lista folytatható). A következő példában a Yuppet használom, mint személyes kedvencemet (a Yupot használom formellenőrzéshez is react-hook-formokkal). Mik voltak az elvárásaim ezzel a hookkal kapcsolatban?
Általános működési mód
Bemenet: Yup séma
Kimenet: Elemzett és ellenőrzött lekérdezés bizonyos típusú biztonsággal és típus hint hibás objektum, ha van.
Az első lépés a logika kivonása a komponensből egy egyedi hookba, amely már kicsit tisztábbá teszi a komponensünk kódját.
export function useParsedQueryString() {
const { search } = useLocation();
const query = qs.parse(search);
return {
query,
};
}
A következő lépés az általános validációs séma hozzáadása bemenetként a hookhoz. Szerencsére a Yup kiváló TypeScript támogatású, így csak ki kell használnunk ezt. Egy általános típusnevet biztosít az ObjectSchema-ra, amelyet könnyen paraméterezhetünk az általános típusunkkal.
export function useParsedQueryString<T extends yup.AnyObject>(
validationSchema: yup.ObjectSchema<T>,
);
Az egyetlen érdekes rész, hogy a generikus (T) paraméter vagy useParsedQueryString kiterjeszti az AnyObject-ot, de ez csak egy típusálnév bármilyen objektumnak string kulcsokkal.
A következő lépés néhány változó létrehozása a lekérdezés aktuális állapotának megtartásához, az első az elemzett és ellenőrzött lekérdezés, a második a hiba lesz. Mindkettő null értékű lehet — ha van hiba, a lekérdezés objektum null lesz, ha nincs hiba, a hiba null lesz.
export function useParsedQueryString<T extends yup.AnyObject>(
validationSchema: yup.ObjectSchema<T>,
) {
const { search } = useLocation();
const [validatedQuery, setValidatedQuery] = useState<T | null>(null);
const [validationError, setValidationError] = useState<yup.ValidationError | null>(null);
useEffect(() => {
const query = qs.parse(search);
setValidatedQuery(result as T);
}, [search]);
return {
query: validatedQuery,
error: validationError,
};
}
A kód utolsó darabja a tényleges ellenőrzés. A Yup séma provide_sync metódust nyújt, mely visszaadja az elemzett (és átalakított!) objektumot, vagy kivételt dob egy ValidationError példányával:
useEffect(() => {
const query = qs.parse(search);
try {
const result = validationSchema?.validateSync(query);
setValidatedQuery(result as T);
} catch (e) {
if (e instanceof yup.ValidationError) {
setValidationError(e);
setValidatedQuery(null);
} else {
throw e;
}
}
}, [search, validationSchema]);
Ezzel készen állunk a vadonatúj keresési lekérdezés validációs hookunk használatára a komponensünkön, ami meglehetősen egyszerű.
Először meg kell konstruálnunk a sémát:
const queryValidationSchema = yup.object({
oobCode: yup.string().required(),
mode: yup.mixed<AuthActionMode>().oneOf(Object.values(AuthActionMode)),
});
Ezután használhatjuk a hookban:
export function CustomAuthActionPage() {
const { query, error } = useParsedQueryString(queryValidationSchema);
if (error) {
return <div>Érvénytelen Paraméterek</div>;
}
return query.mode; // tegyen valamit a paraméterekkel
}
Látja, milyen elegáns ez a megoldás ahhoz képest, amivel kezdtük?
És az utolsó követelményhez, a típusbiztonság: a Yup segítségével a lekérdezési eredmény típusbiztonsága "ingyenes", csak használja:

Látható, hogy ezzel a meglehetősen egyszerű kóddal növelhetjük az alkalmazásunk biztonságát a lehetséges hibáktól (mivel kényszerítve vagyunk az ellenőrzés és a hiányzó vagy rosszul formázott lekérdezések kezelése), és növelhetjük a fejlesztői élményt is a megfelelő típus hintelik segítségével.
(+1) Újrafelhasználható enum séma létrehozása
Tehát az előző példákban az alábbi módszert használtam annak ellenőrzésére, hogy valami enum érték-e.
yup.mixed<AuthActionMode>().oneOf(Object.values(AuthActionMode)),
De mi van akkor, ha különböző enumokat ellenőrzünk a (esetleg nagy) kódbázisunkban? A megoldás nagyon körülményes lehet, és sok boilerplate kódot tartalmaz.
Mi a jobb megoldás erre? A Yupban létrehozhatunk egyéni ellenőrzési metódusokat a yup.addMethod segítségével, mint például:
Yup.addMethod(Yup.mixed, 'enum', function enumSchema(this, enumValue) {
return this.oneOf(Object.values(enumValue));
});
A trükkös rész, hogy hogyan tehetjük ezt meg úgy, hogy a TypeScript kiderítse a megfelelő típusokat a sémánkhoz? Modul bővítés a megmentőnk!
A Yup csomag MixedSchema-t kell bővítenünk egy új "enum" nevű módszerrel, amely kap egy enumot és egy MixedSchema-t (mivel ez a minta a metódus láncolásához) ad vissza
declare module 'yup' {
interface MixedSchema {
enum<T extends EnumValue>(enumValue: T, message?: string): Yup.MixedSchema<T[keyof T]>;
}
}
Yup.addMethod(Yup.mixed, 'enum', function enumSchema<T extends EnumValue>(this: Yup.MixedSchema, enumValue: T) {
return this.oneOf(Object.values(enumValue) as T[keyof T][]);
});
De mi az EnumValue a példában? Alapvetően csak egy segítő interfész, hogy az EnumValue paramétere generikus legyen és fogadjon egy enumot:
export interface EnumValue {
[key: string]: string | number;
}
És ezzel az új egyéni metódussal, a sémában található enum ellenőrzés olvashatóbbá válik, és még mindig megkapjuk a megfelelő típusokat a sémához!
const queryValidationSchema = yup.object({
oobCode: yup.string().required(),
mode: yup.mixed().enum(AuthActionMode)
});
Összefoglalva: megoldás másolásra és beillesztésre
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import * as yup from 'yup';
import qs from 'query-string';
export function useParsedQueryString<T extends yup.AnyObject>(
validationSchema: yup.ObjectSchema<T> | ReturnType<typeof yup.lazy<yup.ObjectSchema<T>>>,
) {
const { search } = useLocation();
const [validatedQuery, setValidatedQuery] = useState<T | null>(null);
const [validationError, setValidationError] = useState<yup.ValidationError | null>(null);
useEffect(() => {
const query = qs.parse(search);
try {
const result = validationSchema?.validateSync(query);
setValidatedQuery(result as T);
} catch (e) {
if (e instanceof yup.ValidationError) {
setValidationError(e);
setValidatedQuery(null);
} else {
throw e;
}
}
}, [search, validationSchema]);
return {
query: validatedQuery,
error: validationError,
};
}
enum AuthActionMode {
RESET_PASSWORD = 'resetPassword',
RECOVER_EMAIL = 'recoverEmail',
VERIFY_EMAIL = 'verifyEmail',
}
const queryValidationSchema = yup.object({
oobCode: yup.string().required(),
mode: yup.mixed<AuthActionMode>().oneOf(Object.values(AuthActionMode)),
});
export function CustomAuthActionPage() {
const { query, error } = useParsedQueryString(queryValidationSchema);
if (error) {
return <div>Érvénytelen Paraméterek</div>;
}
return null; // tegyen valamit a paraméterekkel
}
Szerző: Horváth Márton