שדות דינמיים בטפסים: פיתוח React עם אימות Zod

טפסים עם שדות דינמיים הופכים פעמים רבות לצוואר בקבוק במוצרי SaaS ובמרקטפלייסים: הוספת אלמנטים גורמת ל-renders מיותרים, והאימות מוביל לבאגים בשמירת נתונים. אנחנו בונים טפסים כאלה ב-React באמצעות useFieldArray ו-Zod, ומבטיחים ביצועים יציבים וממשק רספונסיבי. הצוות שלנו מספק את הפרויקט במפתח מלא—מהארכיטקטורה ועד לתמיכה, תוך התחשבות במיון ובתרחישים מורכבים.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
שדות דינמיים בטפסים: פיתוח React עם אימות Zod
בינוני
~2-3 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1359
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1112
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

טפסים עם שדות דינמיים הם תרחיש נפוץ במוצרי SaaS ובמרקטפלייסים. הלקוח מוסיף נוסעים, פריטים או משתפי פעולה, והמפתח מתמודד עם אתגרים של ולידציה, ביצועים ו-UX. במוצר SaaS טיפוסי, טופס עשוי להכיל עד 50 חלקים דינמיים. ללא יישום נכון, כל הוספת שדה גורמת לתגובת שרשרת של רינדורים מחדש — INP יורד ל-500 אלפיות השנייה, מה שקריטי עבור Core Web Vitals. אנו בונים טפסים כאלה ב-React באמצעות const passengerSchema = z.object({ firstName: z.string().min(2, 'Минимум 2 символа'), lastName: z.string().min(2), birthDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), passport: z.string().regex(/^\d{4}\s\d{6}$/), }); ו-Zod, ומבטיחים פעולה יציבה ללא קשר למספר השדות.

למה יישום נכון של שדות דינמיים חשוב

שגיאות במהלך פעולות הוספה/הסרה מובילות לשאילתות N+1, רינדורים מיותרים ובאגים בשמירת נתונים. הצוות שלנו, עם ניסיון של למעלה מעשור, פיתח גישה שממזערת סיכונים אלה. לדוגמה, במרקטפלייס אחד ייעלנו טופס רישום עם 30 שדות — זמן הטעינה ירד מ-4 שניות ל-0.8 שניות בזכות עצי React.memo וולידציה עצלה. בפרויקט אחר עם 5,000 טפסים פעילים, הפחתנו את LCP ב-80% — מ-3.2 שניות ל-0.6 שניות.

"טפסים דינמיים הם מפתח ל-UX גמיש, אך היישום שלהם דורש תשומת לב לפרטים: כל שדה נוסף יכול לעלות אלפיות שנייה של INP." — מהנדס בכיר מפרויקט מסחר אלקטרוני גדול.

שימוש ב-import { useForm, useFieldArray, Controller } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; const passengerSchema = z.object({ firstName: z.string().min(2), lastName: z.string().min(2), birthDate: z.string(), passport: z.string().regex(/^\d{4}\s\d{6}$/), }); const formSchema = z.object({ passengers: z.array(passengerSchema).min(1).max(9), }); type FormValues = z.infer<typeof formSchema>; export function PassengersForm() { const { control, register, handleSubmit, formState: { errors } } = useForm<FormValues>({ resolver: zodResolver(formSchema), defaultValues: { passengers: [{}] }, }); const { fields, append, remove, move } = useFieldArray({ control, name: 'passengers', }); return ( <form onSubmit={handleSubmit(console.log)}> <div className="space-y-4"> {fields.map((field, index) => ( <div key={field.id} className="border rounded-xl p-4 relative"> <div className="flex items-center justify-between mb-3"> <h3 className="font-medium">Пассажир {index + 1}</h3> {fields.length > 1 && ( <button type="button" onClick={() => remove(index)} className="text-red-500 text-sm hover:text-red-700" > Удалить </button> )} </div> <div className="grid grid-cols-2 gap-3"> <div> <label className="block text-sm mb-1">Имя</label> <input {...register(`passengers.${index}.firstName`)} className="input-field" /> {errors.passengers?.[index]?.firstName && ( <p className="text-red-500 text-xs mt-1"> {errors.passengers[index].firstName?.message} </p> )} </div> <div> <label className="block text-sm mb-1">Фамилия</label> <input {...register(`passengers.${index}.lastName`)} className="input-field" /> </div> <div> <label className="block text-sm mb-1">Паспорт</label> <input {...register(`passengers.${index}.passport`)} placeholder="1234 567890" className="input-field" /> </div> </div> </div> ))} </div> {fields.length < 9 && ( <button type="button" onClick={() => append({})} className="btn-secondary mt-4 w-full" > + Добавить пассажира </button> )} <button type="submit" className="btn-primary mt-4 w-full"> Продолжить </button> </form> ); } יעיל פי 3 מניהול מערכים ידני: פחות קוד, פחות רינדורים מחדש, תמיכה מובנית במיון. השוו:

גישה רינדורים מחדש בעת שינוי מורכבות תחזוקה גרירה ושחרור
useFieldArray רק שדות ששונו נמוכה מובנית
State ידני כל השדות גבוהה דורש יישום נפרד

איך אנו מיישמים ולידציה ועיבוד נתונים

אנו משתמשים בסכמת Zod עבור כל רכיב במערך. זה מספק טיפוסי TypeScript וולידציה בצד השרת. דוגמת סכמה לנוסעים:

const passengerSchema = z.object({
  firstName: z.string().min(2, 'Минимум 2 символа'),
  lastName: z.string().min(2),
  birthDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  passport: z.string().regex(/^\d{4}\s\d{6}$/),
});

הולידציה בצד הלקוח מתבצעת מיידית, וצד השרת משכפל אותה. כך מושגת שלמות נתונים של 100%.

יישום באמצעות React Hook Form useFieldArray

import { useForm, useFieldArray, Controller } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

const passengerSchema = z.object({
  firstName: z.string().min(2),
  lastName: z.string().min(2),
  birthDate: z.string(),
  passport: z.string().regex(/^\d{4}\s\d{6}$/),
});

const formSchema = z.object({
  passengers: z.array(passengerSchema).min(1).max(9),
});

type FormValues = z.infer<typeof formSchema>;

export function PassengersForm() {
  const {
    control,
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { passengers: [{}] },
  });

  const { fields, append, remove, move } = useFieldArray({
    control,
    name: 'passengers',
  });

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <div className="space-y-4">
        {fields.map((field, index) => (
          <div key={field.id} className="border rounded-xl p-4 relative">
            <div className="flex items-center justify-between mb-3">
              <h3 className="font-medium">Пассажир {index + 1}</h3>
              {fields.length > 1 && (
                <button
                  type="button"
                  onClick={() => remove(index)}
                  className="text-red-500 text-sm hover:text-red-700"
                >
                  Удалить
                </button>
              )}
            </div>
            <div className="grid grid-cols-2 gap-3">
              <div>
                <label className="block text-sm mb-1">Имя</label>
                <input {...register(`passengers.${index}.firstName`)} className="input-field" />
                {errors.passengers?.[index]?.firstName && (
                  <p className="text-red-500 text-xs mt-1">
                    {errors.passengers[index].firstName?.message}
                  </p>
                )}
              </div>
              <div>
                <label className="block text-sm mb-1">Фамилия</label>
                <input {...register(`passengers.${index}.lastName`)} className="input-field" />
              </div>
              <div>
                <label className="block text-sm mb-1">Паспорт</label>
                <input {...register(`passengers.${index}.passport`)} placeholder="1234 567890" className="input-field" />
              </div>
            </div>
          </div>
        ))}
      </div>
      {fields.length < 9 && (
        <button
          type="button"
          onClick={() => append({})}
          className="btn-secondary mt-4 w-full"
        >
          + Добавить пассажира
        </button>
      )}
      <button type="submit" className="btn-primary mt-4 w-full">
        Продолжить
      </button>
    </form>
  );
}

מיון בגרירה ושחרור

עבור טפסים שבהם סדר הפריטים חשוב (רשימת מוצרים בהזמנה, עדיפויות משימות):

import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';

function SortableFieldItem({ field, index, onRemove }: SortableItemProps) {
  const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: field.id });
  return (
    <div ref={setNodeRef} style={{ transform: CSS.Transform.toString(transform), transition }}>
      <div {...attributes} {...listeners} className="cursor-grab p-1">⠿</div>
      {/* поля элемента */}
    </div>
  );
}

// В основном компоненте:
function handleDragEnd(event) {
  const { active, over } = event;
  if (active.id !== over.id) {
    const from = fields.findIndex(f => f.id === active.id);
    const to = fields.findIndex(f => f.id === over.id);
    move(from, to);
  }
}

תהליך העבודה

שלב מה אנו עושים תוצאה
אנליטיקה לומדים תרחישי מילוי, מספר שדות מקסימלי, דרישות ולידציה מפרט טופס
עיצוב בוחרים סטאק (React Hook Form + Zod + @dnd-kit), מתכננים סכמת נתונים, אב-טיפוס UX תיעוד API, מוקאפים
יישום כותבים קוד עם useFieldArray, ולידציה, מיון טופס עובד במאגר
בדיקות בדיקות יחידה לוולידציה, בדיקות e2e עם Cypress, בדיקות עומס כיסוי של 100%, דוח Core Web Vitals
פריסה CI/CD, הגדרת ניטור, העברת אישורי גישה טופס בייצור, הוראות לצוות

שלב אחר שלב: איך אנו מוסיפים שדה חדש עם רינדור מינימלי

  1. משתמשים ב-import { DndContext, closestCenter } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; function SortableFieldItem({ field, index, onRemove }: SortableItemProps) { const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: field.id }); return ( <div ref={setNodeRef} style={{ transform: CSS.Transform.toString(transform), transition }}> <div {...attributes} {...listeners} className="cursor-grab p-1">⠿</div> {/* поля элемента */} </div> ); } // В основном компоненте: function handleDragEnd(event) { const { active, over } = event; if (active.id !== over.id) { const from = fields.findIndex(f => f.id === active.id); const to = fields.findIndex(f => f.id === over.id); move(from, to); } } עם מזהים ייחודיים (useFieldArray).
  2. עוטפים כל פריט ב-field.id עם השוואת props עמוקה.
  3. לוולידציה, מיישמים React.memo עם בדיקה עצלה — הסכמה נקראת רק עבור שדות ששונו.
  4. בעת הסרה, קודם מפעילים אנימציה על הרכיב, ואז קוראים ל-zodResolver לאחר 300 אלפיות השנייה.

מה כלול

  • קוד מקור של הטופס עם הערות
  • תיעוד API (Swagger/OpenAPI)
  • בדיקות (Jest + React Testing Library)
  • הוראות פריסה ל-DevOps שלכם
  • שבועיים של תמיכה חינם לאחר המסירה
  • אחריות ל-6 חודשים על באגים שזוהו

לוח זמנים ותמחור

לוח זמנים: בין 2 ל-5 ימי עסקים, תלוי במורכבות הולידציה והאינטגרציה. התמחור מחושב באופן אישי — נעריך את הפרויקט שלכם תוך יום אחד. צרו קשר לייעוץ.

איך להימנע מטעויות נפוצות

  • אל תשתמשו באינדקס המערך כ-key — זה שובר את ה-state במהלך מיון. השתמשו ב-id הייחודי מ-useFieldArray.
  • אל תגזימו עם z.union עבור טיפוסים דינמיים — זה מאט את TypeScript. עבור סכמות מקוננות, השתמשו ב-remove.
  • אם השדות עולים על 50, השתמשו בווירטואליזציה (למשל, react-window). אחרת, הדפדפן עלול לפגר.
איך לנפות בעיות ביצועים? הפעילו את React DevTools Profiler, הוסיפו לוגים ב-useEffect כדי לספור זמן רינדור. השתמשו ב-`why-did-you-render` כדי לזהות רינדורים מיותרים.

המומחיות שלנו מבטיחה פעולה יציבה של הטפסים. עם למעלה מ-50 יישומים מוצלחים למרקטפלייסים, בנקים ומערכות CRM, אתם יכולים לסמוך על המסירה שלנו. בקשו פיתוח — נחזור אליכם תוך שעה. קבלו ייעוץ ליישום טפסים עם שדות דינמיים — נבחר את הסטאק המתאים לתרחיש שלכם.

קישור לתיעוד הרשמי: React Hook Form useFieldArray.