Нейросетьнаваялаваммонстра?Внашихпроектахтакоеневыживает

У вас в проекте есть компонент, который вы боитесь трогать? Модалка создания поста в нашем проекте разрослась до 274 строк — типичный «божественный объект», который делал всё сразу. В статье показываю, как мы разрезали монстра на кастомные хуки и мелкие компоненты, сократили код до 70 строк и сделали систему прозрачной. Почему это лучше «вайбкодинга» на нейросетях? Потому что инженерная мысль побеждает генерацию мусора. Примеры кода, схемы и выводы внутри.

Смотреть кейсы
📅 13.02.2026·🔄 Обновлено: 13.02.2026·⏱️ 5 мин·Продвинутый·Разработка

Рефакторинг гигантского компонента: как мы сделали CreatePostModal читаемым и поддерживаемым

Сегодня хочу поделиться историей рефакторинга одного из ключевых компонентов нашего проекта — модального окна создания поста в системе управления контент планом. Изначально этот компонент разросся до 274 строк и превратился в типичного «божественного объекта», который делал всё: управлял формой, обрабатывал выбор платформ, загружал медиа, отправлял данные, показывал ошибки… Вносить изменения в такой код становилось страшно, а новичкам требовалось полдня, чтобы просто разобраться в логике.

В этой статье я покажу, как мы разбили монстра на кастомные хуки и маленькие компоненты, сделав код прозрачным, тестируемым и быстрым. Поехали!

Исходная ситуация: компонент на 274 строки

Вот фрагмент оригинального CreatePostModal.tsx (я сократил его для наглядности, но общая структура сохранена):

export function CreatePostModal({ open, onOpenChange, selectedTeamId, initialScheduledDate }) {
  const currentUser = useStore((s) => s.currentUser);
  const { data: authMe } = useAuthMe();
  const createPostMutation = useCreatePostMutation();
  const [error, setError] = useState(null);
  const { register, handleSubmit, reset, watch, setValue } = useForm({ ... });
  const selectedPlatforms = watch("selectedPlatforms");
  const contentType = watch("contentType");
  const [mediaUrls, setMediaUrls] = useState([]);

  // Эффект для сброса ошибок и подстановки даты
  useEffect(() => { ... }, [open, initialScheduledDate]);

  // Эффект для авто-выбора платформ при смене типа контента
  useEffect(() => { ... }, [contentType]);

  const togglePlatform = (id) => { ... };

  const onSubmit = async (data) => { ... };

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>...</DialogHeader>
        {error && <Alert ...>...</Alert>}
        <form onSubmit={handleSubmit(onSubmit)}>
          {/* Поле заголовка */}
          {/* Поле типа контента */}
          {/* Блок выбора платформ (рендерится 8 кнопок с условиями) */}
          {/* Поле содержания */}
          {/* Поле даты */}
          {/* Поле приоритета */}
          <MediaUpload ... />
        </form>
        <DialogFooter>...</DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Проблемы такого подхода очевидны:

  • Низкая читаемость — перемешаны логика и представление.

  • Сложность тестирования — невозможно протестировать выбор платформ отдельно от отправки формы.

  • Лишние ререндеры — изменение одного поля (например, даты) перерисовывает всё окно.

  • Трудности с поддержкой — добавление новой платформы требует правки сразу в трёх местах.

Рефакторинг: разделяем ответственность

Мы решили выделить три уровня:

  1. Кастомные хуки — для бизнес-логики и состояния.

  2. Маленькие UI-компоненты — для отрисовки частей интерфейса.

  3. Главный компонент — только композиция.

📦 Шаг 1. Выносим логику формы в хук useCreatePostForm

Теперь вся работа с react-hook-form, сабмит и управление ошибками живут отдельно:

// hooks/useCreatePostForm.ts
export function useCreatePostForm(initialScheduledDate?: Date | string | null) {
  const currentUser = useStore((s) => s.currentUser);
  const { data: authMe } = useAuthMe();
  const createPostMutation = useCreatePostMutation();
  const [error, setError] = useState<string | null>(null);
  
  const form = useForm({
    defaultValues: {
      title: "",
      content: "",
      scheduledDate: new Date().toISOString().split('T')[0],
      contentType: "",
      selectedPlatforms: [],
      priority: "",
    }
  });

  // Подстановка initialScheduledDate при открытии
  useEffect(() => {
    if (initialScheduledDate) {
      const date = typeof initialScheduledDate === "string"
        ? initialScheduledDate.split("T")[0]
        : format(initialScheduledDate, "yyyy-MM-dd");
      form.setValue("scheduledDate", date);
    }
  }, [initialScheduledDate, form.setValue]);

  const onSubmit = async (data) => {
    setError(null);
    const authorId = currentUser?.id ?? authMe?.id;
    if (!authorId) {
      setError('Не удалось определить автора');
      return;
    }
    try {
      await createPostMutation.mutateAsync({ ...data, authorId });
      form.reset();
      setError(null);
      return true; // сигнал об успехе
    } catch (err) {
      setError(err.message);
    }
  };

  return {
    ...form,
    error,
    onSubmit: form.handleSubmit(onSubmit),
    isPending: createPostMutation.isPending,
  };
}

Шаг 2. Логика платформ — в хук usePlatforms

Управление списком доступных платформ и авто-выбор при смене типа контента:

// hooks/usePlatforms.ts
const platforms = [ /* массив платформ */ ];
const defaultPlatformsByType = { ... };

export function usePlatforms(selectedType: string, onChange: (platforms: Platform[]) => void) {
  const [selected, setSelected] = useState<Platform[]>([]);

  // Авто-выбор при изменении типа
  useEffect(() => {
    if (selectedType && defaultPlatformsByType[selectedType]) {
      const defaults = defaultPlatformsByType[selectedType];
      setSelected(defaults);
      onChange(defaults);
    }
  }, [selectedType, onChange]);

  const togglePlatform = (platform: Platform) => {
    const newSelected = selected.includes(platform)
      ? selected.filter(p => p !== platform)
      : [...selected, platform];
    setSelected(newSelected);
    onChange(newSelected);
  };

  return { platforms, selected, togglePlatform };
}

Шаг 3. UI-компоненты

Теперь каждый крупный блок интерфейса стал отдельным компонентом:

PlatformSelector.tsx — отвечает только за отрисовку кнопок платформ:

export function PlatformSelector({ selected, onToggle }) {
  return (
    <div className="flex flex-wrap gap-2">
      {platforms.map(platform => (
        <div
          key={platform.id}
          onClick={() => onToggle(platform.id)}
          className={cn(
            "cursor-pointer px-3 py-1.5 rounded-full text-xs font-medium",
            selected.includes(platform.id) 
              ? platform.color + " text-white"
              : "bg-background border-border"
          )}
        >
          {platform.label}
        </div>
      ))}
    </div>
  );
}

ContentTypeSelector — выбор типа контента:

export function ContentTypeSelector({ value, onChange }) {
  return (
    <Select value={value} onValueChange={onChange}>
      <SelectTrigger>
        <SelectValue placeholder="Выберите тип контента" />
      </SelectTrigger>
      <SelectContent>
        <SelectItem value="article">Статья</SelectItem>
        <SelectItem value="post">Пост</SelectItem>
        <SelectItem value="video">Видео</SelectItem>
      </SelectContent>
    </Select>
  );
}

Аналогично создаём DatePicker, PrioritySelector и т.д.

Итоговый главный компонент

После всех выделений CreatePostModal превращается в лаконичную композицию:

export function CreatePostModal({ open, onOpenChange, selectedTeamId, initialScheduledDate }) {
  const {
    register,
    setValue,
    watch,
    error,
    onSubmit,
    isPending,
    reset
  } = useCreatePostForm(initialScheduledDate);

  const contentType = watch("contentType");
  const selectedPlatforms = watch("selectedPlatforms");

  const { platforms, togglePlatform } = usePlatforms(contentType, (newPlatforms) => {
    setValue("selectedPlatforms", newPlatforms);
  });

  // Сброс при закрытии
  useEffect(() => {
    if (!open) {
      reset();
    }
  }, [open, reset]);

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent>
        <DialogHeader>Добавить новый контент</DialogHeader>
        {error && <Alert variant="destructive">{error}</Alert>}
        
        <form onSubmit={onSubmit} className="grid gap-4">
          <Field label="Заголовок">
            <Input {...register("title")} />
          </Field>

          <Field label="Тип контента">
            <ContentTypeSelector
              value={contentType}
              onChange={(v) => setValue("contentType", v)}
            />
          </Field>

          <Field label="Платформы">
            <PlatformSelector
              selected={selectedPlatforms}
              onToggle={togglePlatform}
            />
          </Field>

          <Field label="Содержание">
            <Textarea {...register("content")} />
          </Field>

          <Field label="Дата публикации">
            <Input type="date" {...register("scheduledDate")} />
          </Field>

          <Field label="Приоритет">
            <PrioritySelector
              value={watch("priority")}
              onChange={(v) => setValue("priority", v)}
            />
          </Field>

          <MediaUpload
            value={watch("mediaUrls")}
            onChange={(urls) => setValue("mediaUrls", urls)}
            disabled={isPending}
          />
        </form>

        <DialogFooter>
          <Button variant="outline" onClick={() => onOpenChange(false)}>Отмена</Button>
          <Button type="submit" onClick={onSubmit} disabled={isPending}>
            {isPending ? "Добавление..." : "Добавить"}
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

Теперь главный компонент занимает ~70 строк и читается как оглавление: сразу видно, из каких частей он состоит и какая логика задействована.

Что мы выиграли?

  1. Читаемость — каждый файл отвечает за одну задачу. Чтобы понять, как работает выбор платформ, открываешь usePlatforms и PlatformSelector.

  2. Тестируемость — можно покрыть тестами хук usePlatforms (проверить авто-выбор) и компонент PlatformSelector (проверить рендер кнопок) независимо.

  3. Производительность — благодаря хукам и изоляции состояния, изменения в поле даты не перерисовывают список платформ. React обновляет только нужные части.

  4. ПереиспользуемостьPlatformSelector и ContentTypeSelector можно использовать в других модалках (например, при редактировании поста).

  5. Лёгкость расширения — добавить новую платформу? Правим массив в usePlatforms и цвет — всё.

Вывод

Большие компоненты — зло. Они усложняют разработку и поддержку, особенно в растущей CRM. Разделение на кастомные хуки и мелкие UI-компоненты не только уменьшает количество строк в одном файле, но и делает код предсказуемым и быстрым. Не бойтесь рефакторить: инвестиции в чистоту архитектуры окупаются сторицей.

Попробуйте применить этот подход в своём проекте — и вы увидите, как исчезает «магия» и приходит прозрачность. Удачного кодинга!

А если вам нужно сделать качественный, быстрый, оптимизированный сайт - то обращайтесь к нам через заявку!

Хотите увеличить продажи?

Мы поможем вам разработать и реализовать эффективную стратегию продвижения для вашего бизнеса.