Галоўная / Артыкулы

Кераванне агентаў кодавання на базе AI за дапамою чатырох архітектурных патэранаў React

Выучыце чатыры шаблону React (складаванне секцый, спецыяльныя хукі, поліморфныя компаненты, редактары) і тое, як правільна назва ў запитах дапамагае атрымаць чыстейшы код, створаны AI.

Выдавец: REACTAPP.TOP12 хв чытання2819 слоў

Крыніцы: How I Get AI to Write Cleaner React Code with 4 Architecture Patterns

Агенты для кодавання на базе AI, такія як Claude чыр OpenCode, можу стварыць рабочы функцыянал React за калькі паведамленняў, і код зазвычай выканае тое, пра што вы прасілі. Слабы момент — структура: створаныя компоненты часта складаюцца з сотняў ліній, у якіх маркап, даныя, эфекты і працэўнікі знаходзяцца ў адным файле, а просьба да агента «зробіць яго модульным» часта прыводзіць да стварэння фрагментаў, прызначаных толькі для конкрэтнай стороніцы, а не да практычна відновімых частак. У этай статыце рассказваецца пра чатыры патэрны, якія відлучаюць гэтую проблему з структуры, і, што не менш важна, показваецца, як называць іх у своіх паведамленнях, каб агент стварыў тую архітэктуру, яку вы планавалі, а не выбраў ёю сам.

Чаму рабочы код не тое ж самае, што і хорашая архітэктура

Адзённыя правілы чыстага коду заключаюцца у тым, каб компанента React складалася з працоўна 100 ліній, а практычны верхнія межа — 200 ліній. Це скорэй гіюрістыка, чым закон, але файлы з 500 ліній і больш — гэта самэ тое месца, дзе робота AI пачынае спяшваць, калі яе не кармічаць.

Вочымаючы адпаведны адказ — запытаць агента пра разбіўку элементаў на часткі. Ян зазвычай пагадзіцца і разбіе інтерфейс на кальколька меньшых компанентаў у папцы, прызначанай для таго конкрэтнага абаротку. Хоць гэтыя часткі меньшы, яны все рава належаць да адной функцыі і рэдка можна выкорыстаць вядзома ў іншых месцах. Гэта ёсць дэкомпанаванне, а не модулярнасць.

Для невялкага проекту гэта практычна не мае значэння. Для програмнага забезпечэння, якое павінна развивацца гады, гэта значыць, што кожныя змены паўтараюцца ў кодзе, які ніколі не быў спроектаваны для паўторнага вжывання чы розшырэння. Рашэнне не ў тым, каб перастаць вжываць AI. Ёго — знаць шаблоны проектавання, якія дапамагаюць кодбазе React растаць, і кераваць агентам з ўжываннем іх. Генераванне коду стае дышэўным; рашэнне пра тое, як яго структураваць, вы все ўсё прыносіце самі. Якщо хочаце болей шырокага агляду таго, як выходы AI часта ігнаруюць прынцыпы чыстага коду, адзірніце колі AI піша ваш React-дзеўайс, але ігнаруе прынцыпы чыстага коду.

Шаблон 1: раздзеліце секцыі на чатыры компоненты

Якщо папрабавіце у агента частку UI, яна зазвычай разам выкладзе кантэйнер, загалоўку, адрасаванне спісу і адзінлівыя элементы ў аднай складовай. Падражненне стае відчутным, калі дапрацоўка расте, таму што тая ж структура павтараецца практычна ўсюды: блогі, інтэрнет-магазіны, портфелі, каталогі, стартовыя сторанкі SaaS і панелі керування.

Уявіце стартовую сторанку інтэрнет-магазіна з часткамі для новых, популярных і найболей праблімовых продуктав. Кожная частка выглядае ідэнтычна; разлічаюцца толькі даны. Кожную з іх можна разбіць на чатыры часткі, кожная з яых виконвае адзін завадак:

  • кантэйнер часткі
  • загалоўка часткі
  • спіс картак
  • адзінная карточка

Частка

Частка ёсць «родным» элементам. Яна прыймае даны через props і перадае іх своім дзецям, таму яна ведае пра ўтворэнне структуры, але не ведае ніч пра деталі стылю.

export function ProductSection({ title, subtitle, products }) {
  return (
    <section>
      <SectionHeading
        title={title}
        subtitle={subtitle}
      />

      <ProductList products={products} />
    </section>
  )
}

Загалоўка

Спецыяльны компанент загалоўкі зберагае стылізаванне заглавля і падзаглавля ў аднам месцы, таму кожны раздел у дапынге выглядае аднообразна, без неабходнасці парадзельнага стварэння маркапу.

export function SectionHeading({ title, subtitle }) {
  return (
    <div>
      <h2>{title}</h2>
      <p>{subtitle}</p>
    </div>
  )
}

Спіс

У спіса є адна задача: пераглядаць колекцыю і адрасаваць карточку для кожнага элемента, выкарыстоўваючы стабільны key.

export function ProductList({ products }) {
  return (
    <div className="grid">
      {products.map(product => (
        <ProductCard
          key={product.id}
          product={product}
        />
      ))}
    </div>
  )
}

Заўважыце, што ён не мае жадных уявленняў пра тое, як выглядае продукт. Ён ведае толькі, як распакаваць спіс, і самэ гэта дазволяе лёгка аднарабатваць яго з іншай карточкай пазней.

Карточка

Карточка адражае толькі аднаго элемента і нічога больш.

export function ProductCard({ product }) {
  return (
    <div>
      <img src={product.image} />
      <h3>{product.title}</h3>
      <p>${product.price}</p>
    </div>
  )
}

Ейдзекаванне — гэта ўсё, чым яна керуецца. У рэальным кодзе таксама трэба было б даць зяўроцу значымы атрыбут alt, якога не ўключанаў мінімальны прыклад.

Порэванне двух падходаў

Якщо застаўці ўшыткі на сваіх значэннях за замовчаннем, агент часта стварае ўтаманне такое:

function Products({ products }) {
  return (
    <section>
      <h2>Latest Products</h2>

      <div className="grid">
        {products.map(product => (
          <article key={product.id}>
            <img src={product.image} />
            <h3>{product.title}</h3>
            <p>${product.price}</p>
          </article>
        ))}
      </div>
    </section>
  )
}

Тэхнічна сторона гэтага не мае нічога падабнага да проблемы. Косцы становяцца зрозумелыя пазней, калі іншая сторонка патрабуе той самы стыль загалоў, дизайн карточак або сітку. У такі моменты трэба альбо дублюваць маркап, альбо выкарыстоўваць компаненты з кодавой базы, якая вже сформавалася ўнаследак такога дубліравання.

За дапамогою раздзелення на чатыры компаненты, сама сторонка сведваецца да дадзэнняў і ўзаемнага спалучэння:

function HomePage() {
  return (
    <>
      <ProductSection
        title="Hot Products"
        subtitle="Most popular products right now"
        products={hotProducts}
      />

      <ProductSection
        title="New Products"
        subtitle="Recently added to our store"
        products={newProducts}
      />

      <ProductSection
        title="Trending Products"
        subtitle="What everyone is buying"
        products={trendingProducts}
      />

      <ProductSection
        title="Discounted Products"
        subtitle="Best deals this week"
        products={discountedProducts}
      />
    </>
  )
}

Структура файла застаецца маленькай і прыемлівай:

components/
│
├── SectionHeading.tsx
├── ProductCard.tsx
├── ProductList.tsx
└── ProductSection.tsx

Кожны элемент тепер мае чыста вялікі функцыянал. Той самы загалоў можа выкарыстоўваться ў будзь-ям раздзеле, новы ряд з таварамі — это ўсьмо што іншае ProductSection, а цэлы новыя сторонкі можна склаць з тых сабе ж элементаў.

Запрос пра гэта

У зямест на тое, каб прабавіцца прыказаць агенту стварыць раздзел з таварамі, неабяжна чыткаа апісаць структуру: «Ўжывайце шаблон UI з 4 складовымі: ProductSection, які складаецца з SectionHeading і ProductList, які відображае элементы ProductCard; кожная з складовых виконвае толькі адну задачу». Гэты ўсьго одны рэчыз прыводзіць да іншага формата выходных даных, і рэзультат стае набагато простым для чытання, павторнага выкарыстоўвання і адтрымкі.

Шаблон 2: перанесенне логіки ў спецыяльныя хукі

Большасць разрабоў React знае спецыяльныя хукі, але калі агент виконвае напісанне коду, іх лёгка забыць. Калі прасіюць стварыць новы экран, агент зазвычай залишае стан, эфекты, вызовы API і обробнікі змян у самай складовой. Чэрез калькі ітерацый файл перастае мяць 300 ліній, таму што UI і логіка з’яўляюцца ў сплетенні.

Іноды ўтримванне логіки ў самам компоненте ёсць прыемным рашэнням. Але калі тая ж логіка патрабуецца ў іншых частях коду, або калі яна пачынае ускладняць розумэнне таго, што атрыбуе компонент, зазвычай краща вынесці яе у спецыяльны хук. Типовыя прыклады такіх хукоў:

  • useDebounce() — для затрымкі вводу у пошуку пры выправленні запиту
  • useApi() — для керавання процэсам зялучэння дадзеных, а таксама станамі завантажэння і падачы адмов
  • useLocalStorage() — для зберагчэння стану ў браузеры
  • useClickOutside() — для закрыцья выпадаючых меню чы режымаў, калі корыстнік клікае ў іншы раён
  • usePagination() — для керавання станамі сторунак
  • useToggle() — для керавання булевымі станамі, такімі як адкрытыя бокавыя панелі

Логіка пошуку ўнутрь компонента

Разглянем падзец для пошуку продуктав. Тыповая версія, створаная автоматычна, зберагае як сам запит, так і його адаптаваную версію ў стацыянарным стане, выкарыстоўвае адны эфект з таймерам для апдэйта адаптаванага значэння, а другі эфект — для запрашоўкі даных кожны раз, калі значэнне зміняецца:

function SearchProducts() {
  const [query, setQuery] = useState("");
  const [debouncedQuery, setDebouncedQuery] = useState("");

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedQuery(query);
    }, 500);

    return () => clearTimeout(timer);
  }, [query]);

  useEffect(() => {
    if (!debouncedQuery) return;

    fetch(`/api/products?search=${debouncedQuery}`);
  }, [debouncedQuery]);

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search products..."
    />
  )
}

Гэта працуе. Але якщо трое экранаў патрабуюць таго жяго адпрацавання запытаў, трэба альбо скопіюваць код тры разы, альбо пазней платіць як час, так і ресурсы за його выкарыстоўванне. Таксама існуюць два моменты, якія павышаюць надзеяўнасць: запит трэба прадаць через encodeURIComponent пры ўкладанні ў URL, а рэальная запрашоўка даных павінна кераваць ситуацыямі, кали адпаведзі надходзяць не па порядку, напрыклад, за дапамогою AbortController пад час ачысцэння эфекта.

Выкарыстоўванне хука для адпрацавання запытаў

Логіка таймера пераводзіцца ў самастоятны хук, які прымеў значэнне і час затрымкі та вяртае адаптаванае значэнне:

// hooks/useDebounce.ts

export function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value)
    }, delay)

    return () => clearTimeout(timer)
  }, [value, delay])

  return debouncedValue
}

Функцыя чысткі вычышае запланаваны таймер кожны раз, калі значэнне зміняецца прытаму пасля заканчэння затрымкі, і самэ гэта робіць яе функцыяю дебаусінгу, а не простай затрымкі. Уключэнне delay у массив залежнасцей дапамагае падтрымаць правільную працю хука, якщо калекач зменіў яго.

Пасля гэтага компонент ператвараецца ў тое, чым насправді яўляецца:

function SearchProducts() {
  const [query, setQuery] = useState("")

  const debouncedQuery = useDebounce(query)

  useEffect(() => {
    if (!debouncedQuery) return

    fetch(`/api/products?search=${debouncedQuery}`)
  }, [debouncedQuery])

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
    />
  );
}

Рэндарынг застаецца ў самом компоненте, а механізм дебаусінгу — у хуку; кожны з іх мае свою адпаведальнасць.

Калі хук ўжо неабходны

Не кожны вызов useState() заслуговае на свой хук. Хорашыя прамавы для стварэння хука ўключаюць:

  • тая ж логіка ўжо існуе або будзе існаваць у калькіх компонентах;
  • компонент стае занадта дзьвігатым;
  • функцыянал аб’еднвае калькі вызоў useState(), эфектаў або обробнікаў змян.
  • Выклікі API, формы, функцыя пошуку, пагінацыя, аутантыкач і код для зберагачвання дадзеных заплямавляюць логіку адрасавання;
  • Вы хочаце працаваць з гэтай логікай незалежна ад інтерфейса.
  • Як прасіць пра гэта

    Паказваючы, што логіка павінна застаўцца ў самым коде, ясна зазначыце: "Павінныя быць перадаўнае выкарыстоўваныя функцыі для гэтай можнасці трэба разместіць у спецыяльных хукі, напрыклад useApi(), useDebounce() або useLocalStorage(); компонент жа павінен адпавядаць толькі за форматаванне тэксту." Гэтае правіла дапамагае у всіх чатырох падходах: чым больш інфармацыі пра архітектуру вы даеце, тым кращы будзе створаны код.

    Падход 3: адна поліморфная компанента заместо майже ідэнтычных

    Мета перадаўнае выкарыстоўваных компанентов — не ў тым, каб іх было больш; гэта способ заўтромліць дубляванне. Агенты часта ствараюць такія сем’і компанентоў:

    Button.tsx
    LinkButton.tsx
    IconButton.tsx
    

    Эты элементы раздзеляюць практычна всі свае стылі. Разлікаецца тое, канфегурацыю якога элемента яны атрыбутуюць: іноды рэальнага <button>, іноды элемента anchor, іноды ссылкі router, а іноды толькі элемента-іконкі. Патэрн поліморфізу заменяе цю групу на адну складовую, якая можа атрыбутавацься якім-небудзь элементам, якого трэба ў данай ситуацыі.

    Павтораная версія

    Тыпова пара, створаная такім чынам, выглядае так:

    // Button.tsx
    export function Button({ children }) {
      return (
        <button className="btn">
          {children}
        </button>
      )
    }
    
    // LinkButton.tsx
    export function LinkButton({ href, children }) {
      return (
        <a href={href} className="btn">
          {children}
        </a>
      )
    }
    

    Стылі павторуюцца толькі таму, што зменіўся базовы HTML-элемент.

    Поліморфны элемент Button у TypeScript

    У варыянце, запісанаму у формате тексту, дапраўна прыймае необавязковая атрыбут as, тип якога — гэнерычны ElementType, які за замовчаннем ўзимае значэнне "button". Іншыя атрыбуты вылучаюцца з элемента чыста компонента, на які пасылае as, за дапамою ComponentPropsWithoutRef; сам атрыбут as не павінен быць указаны, каб утрымацца ад суперсчэплення.

    type ButtonProps<E extends ElementType = "button"> = {
      as?: E
      children: ReactNode
      variant?: "primary" | "outline"
    } & Omit<React.ComponentPropsWithoutRef<E>, "as">
    
    const Button = <E extends ElementType = "button">({
      as,
      children,
      variant = "primary",
      ...rest
    }: ButtonProps<E>) => {
      const Component = as || "button"
    
      return (
        <Component {...rest}>
          {children}
        </Component>
      )
    }
    

    Паколькі типы атрыбутаў паследуюць E, перадача значэння as="a" робіць href валідным, а перадача элемента Link з рутэрам робіць його атрыбут to валідным, пры чым адбываецца перакантрольванне типу. У прыкладзе показана спрощаная версія: variant прыймаецца, але ўсё яшчэ не застаўляецца, таму ў повнай версіі яго будзе супарабатаваць з іменем класу, а таксама будзе сумаваны ўсі className, якія былі пераданы. Таксама варта задаць значэння type="button" як стандарт пад час відраслевага стварэння кнопкі, каб яна не випадкова не надасіла форму, якая ёй адпаведае.

    Цей жа компонент тепер пакрывае ўсі варыянты:

    <Button>
      Save
    </Button>
    
    <Button
      variant="outline"
    >
      Cancel
    </Button>
    
    <Button
      as="a"
      href="/pricing"
    >
      Pricing
    </Button>
    
    <Button
      as={Link}
      to="/dashboard"
    >
      Dashboard
    </Button>
    
    <Button
      as="button"
      onClick={handleDelete}
    >
      Delete
    </Button>
    

    Функціі “Зберагчы”, “Анулюй”, лінк з цэнамі, маршрут дашборду і дзеянне выдалення — усе яны маюць аднойчы дизайн; зміняецца толькі элемент, який відображаецца, або його прыемна.

    Калі яго вжываць

    Простыя тэст: якщо інтэрфейс застаецца тым жа, але базавы элемент зменяецца, то такі компонент ўзгоджваецца з панявамі поліморфнай структуры. Тыповыя прыклады:

    • кнапкі, якія іноды выступаюць як хутчэўкі
    • компоненты для тыпаграфіі, якія атрыбутуюць h1, h2, p або span
    • карты, якія іноды ўжо ёсць article, а іноды — клікабельныя хутчэўкі
    • контейнеры для макетаў, якія атрыбутуюць section, div або main

    Гэта не якаясь індывідуальная праганацыя. Бібліятэкі компонентаў, такія як Radix UI, Chakra UI і Flexy UI, значна паслужаюцца гэтай ідэяй для стварэння гнучкіх систем дизайна.

    Стварэнне умов для ўжыцця

    Агенты частаца ствараюць новы компонент для кожнай незначной разніцы, таму дайце ўсём ім шаблон па імені: «Напісце адны поліморфны Button, які прыймае атрыбут as і можа выканаць відображэння натыўного button, тага a або элемента React Router Link; керуйце варіантамі і размерамі ўнутрь гэтага адного компонента, замест таго каб ствараць новыя». У такім разе будзе меньш компонентоў, менш дуплікацый і болей цяперозрозумелая система дизайна.

    Шаблон 4: адзінаванне супакоўзаных стаў з дапамогою редуктара

    Гэты, верагодна, шаблон, які агенты найбольш ігнаруюць. Їх стандартны падход — адзінокі useState() для кожной значэння, што падходзіць для простых компонентоў. Але калі функцыянал расте, вы працуе з пяцьма-шасцю елементамі стаў, якія залежнае адзін ад другога:

    const [loading, setLoading] = useState(false);
    const [products, setProducts] = useState([]);
    const [error, setError] = useState(null);
    const [page, setPage] = useState(1);
    const [search, setSearch] = useState("");
    const [sortBy, setSortBy] = useState("latest");
    

    Калі пагінацыя, фільтрацыя, сортаванне, завантажэнне і обробка адзінаў выконваюцца адночасна, зазвычай адна дзеянне пользователя трэбае адразу апডейтаваць калькі з іх, і такі компаненты стаюць складнымі для розумення.

    Скоординаваныя функціі-зменнікі

    Ось як выглядае вызов fetch з адзінамі станамі:

    setLoading(true)
    
    try {
      const data = await fetchProducts()
    
      setProducts(data)
      setError(null)
    } catch {
      setError("Something went wrong")
    } finally {
      setLoading(false)
    }
    

    Дзеякі функціі-зменнікі запускаюцца адночасна для адного логічнага запуску. Гэта групаванне ўсё часта ёст клясічны знак таго, што редюсер мог бы краща выразіць гэтую логіку.

    Моделюванне пераходаў як дзеянняў

    За дапамогою редюсера вы описваеце форму стану адназначна і перыяводзіце пераходы, якія ён можа здарыцца. Синтаксіс будзе знакомы, якщо вы вжывалі Redux:

    const initialState = {
      loading: false,
      products: [],
      error: null,
    };
    
    function reducer(state, action) {
      switch (action.type) {
        case "FETCH_START":
          return {
            ...state,
            loading: true,
          };
    
        case "FETCH_SUCCESS":
          return {
            loading: false,
            products: action.payload,
            error: null,
          };
    
        case "FETCH_ERROR":
          return {
            ...state,
            loading: false,
            error: action.payload,
          };
    
        default:
          return state;
      }
    }
    

    FETCH_START задае флаг загрузкі, FETCH_SUCCESS зберагае таварыны і ацэхвае як флаг загрузкі, так і паведамленні пра адзінакоў, а FETCH_ERROR фіксуе адзінакоў. У случае успеху вяртаецца цэлы новы об’ект; якщо пазней да стану дадаць такія поля, як page або sortBy, не забудзьце там таксама выкарыстоўваць ...state, інакш гэтыя значэнні будуць прыгародзены.

    Потым компонент выканае дзеянні замест таго, каб вызываць функцыі-зменнікі:

    const [state, dispatch] = useReducer(
      reducer,
      initialState
    )
    
    dispatch({ type: "FETCH_START" })
    
    dispatch({
      type: "FETCH_SUCCESS",
      payload: products,
    })
    

    Кожная апдэйтаванне тепер праходзіць чераз адну функцыю. Ёсць разумець, як працуе гэтая функцыя, трэба чытаць редукер, а не шукаць вызывы setState у всім коде компонента, і адтуды, што редукер — це чыстая функцыя, яго лёгка тэставаць у відэ наявнасць.

    Калі падходзіць редукер

    Редукеры не падходзяць для кожнага компонента. Яны становяць сэнс тады, калі:

    • Калькі стану зменшаюцца ў адно;
    • Функцыя мае багато разных дзейсненняў;
    • Пераходы стану стаюць важкімі да адстэпавання;
    • Функцыя ўключае корзіну, багатоэтапны візард, процес абмовкі, большы форму чыта актыўную панель керування.

    Для модальных вікнаў чы переключэння просты useState() застаецца кращым выборам.

    Запит у агента

    Без керавання агент практычна завжды будзе выконваць множлівасць вызоваў useState(). Запытайце пра жаданую структуру: "Рэалізуйце гэта за дапамогою useReducer: задайце іменаваныя типы дзейсненняў, пакладзіце кожны зменшэння стану ў функцыю редуктара, і нехай компонент рабіць толькі відрасліванне і адправку сігналаў." Чым больша функцыя, тым болей гэта дае рэзультатаў.

    Ключовыя выводы

    • Агенты AI надзеямо ствараюць рабочы код на React, але часта выбіраюць монолітныя компаненты, логіку безпосередна ў кодзе, майже ідэнтычныя варіанты та розрасцянуты статус.
    • Чатыры патэрна дапамагаюць расправіцацца з гэтымі стандартнымі рашэннямі: складанне секцый, спецыяльныя хукі, поліморфныя компаненты та редакторы.
    • Кожны патэрн мае чыстаючы фактар; яго трэба застосавляць, калі з’яўляецца гэты фактар, а не інстынктыўна.
    • Называнне патэрна та элементаў, якія вы чакаеце у своем запите, — гэта самы просты спосаб здобыць кращую архітектуру ад агента.
    • Стварэнне коду становіцца простым з кожным годам; адлучэнне, якім чынам ён павінен быць структураваны, застаецца задачай разработчыка, а знанне гэтых патэрнаў дапамагае яму ў гэтым.