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

Адзін форма запрошэння для Server Actions, createServerFn і tRPC

Адзін форма запрошэння створана трыма способамі: Next.js Server Actions, TanStack createServerFn і tRPC; аналізаваліся тыпы, пакеты, працэўнасць без JS і памылкі Zod.

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

Крыніцы: One Form, Three Backends: Server Action, createServerFn, tRPC.

Тры ўстаўкі, адна польцо для электронной пошты

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

Інтэрфейс ўжо дужа просты. Адна польцо email для введэння. Нажміце «Адправіць». Успех паказваецца як лінія пад кнопкай, а неудача — таксама лінія пад кнопкай. Гэта ўсё, што ёсць у продукцыі.

Працоўнікі: Next Server Action, TanStack createServerFn і трансфармацыя tRPC. Адна спакульная схема Zod. Адна спакульная копія. Той самы прыклад аплікацыі з чатырма маршрутамі, плюс дрэва Start, каб createServerFn не быў непатрэбным элементам. Маніфесты пра тое, якая устаўка ёсць «стандартам 2026 года», не дапамагаюць форме. Форма цікавіцца тым, чыры разлучэнняя дадзеныя прыходзяць у відформатаваных строчках, чыры тыпы застаюцься незменнымі і чыры стораніца працюе без кліентскага JavaScript.

Што на самай працы обяцвае кожная устаўка

У Next пазначыце функцыю з "use server" і накіруйце на яе атрыбут action формы — за жаданнем праз useActionState для статуса у працэсе выканання і рэзультата.

У TanStack Start функцыя createServerFn вяртае тыпаваную функцыю, якая атрыбуеўча перакананне. Калі яе вызваць з стандартнай пасылкі формы, трэба вікорыстоўваць сабскі адаптар; за замовчаннем я не падае як дзейснасць прогрэсіўнай формы.

У tRPC вы задаеце процедуру мутацыі і вызываеце яе з кліента. Работа без JavaScript не ўключана ў такую структуру.

Этыя опублікованые контракты стварылі адлогікі эксперымента.

Шэма, якае вікорыстоўваецца всюды

export const inviteSchema = z.object({
  email: z.string().email(),
});
export type InviteInput = z.infer<typeof inviteSchema>;

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

Шлях дзейснасці сервера

"use server";

import { inviteSchema } from "./schema";

export async function invite(prev: { error?: string } | null, formData: FormData) {
  const parsed = inviteSchema.safeParse({
    email: formData.get("email"),
  });
  if (!parsed.success) return { error: parsed.error.issues[0].message };
  await db.invites.create(parsed.data.email);
  return { error: undefined, ok: true };
}
"use client";
import { useActionState } from "react";
import { invite } from "./actions";

export function InviteForm() {
  const [state, action, pending] = useActionState(invite, null);
  return (
    <form action={action}>
      <input name="email" type="email" required />
      <button disabled={pending}>Invite</button>
      {state?.error ? <p>{state.error}</p> : null}
    </form>
  );
}

Калі JavaScript выключаны, форма все равна адправляе даныя. Гэта і ёсь кантракт Action, які выкарыстоўваецца ў гэтым лабараторным прыемлі.

шлях createServerFn

export const inviteFn = createServerFn({ method: "POST" })
  .inputValidator(inviteSchema)
  .handler(async ({ data }) => {
    await db.invites.create(data.email);
    return { ok: true as const };
  });

Пад час запуску форма вызывае inviteFn({ data: { email } }) з onSubmit. Без дадатковых адаптараў для гэтага шляху патрэбны JavaScript-функцыі. Стварэнне фальшывага кэпсулі для прогрэсіўнага павышэння якосты і выдаванне яго за частку фрэймворка могло б спамсаваць практычныя прыклады порэвання.

шлях tRPC

invite: publicProcedure.input(inviteSchema).mutation(async ({ input }) => {
  await db.invites.create(input.email);
  return { ok: true as const };
}),

У кліента выкарыстоўвалася функцыя api.invite.useMutation(). Тыпы для input.email былі выключныя. Без JavaScript кнопка не мае жаднае функцыональнасці.

Рэзультаты трох падходаў

Тыпы. tRPC і createServerFn выважваюць даныя вхіду з Zod без неабяжнасці яшчэ аднаго тыпу кліента. Функцыя сервера прымала FormData і выкарыстоўвала safeParse. Гэта безпечна, але не тое ж сама, як вызов кліента з пазначанымі тыпамі.

Пакет. Размер “острова” функцыі практычна не збільшыўся. tRPC выкарыстоўвае тэкстуры кліента/прадаўцы, якія ужо ёсць частынай API. createServerFn выкарыстоўвае функцыю кліента Start. Ужытак існуючага tRPC робіць мутацыю дэшавай; стварэнне новага tRPC толькі для адной электранадзесланніцы — нядаўна.

Выкліканне адхыленняў. Калі db.invites.create прымушана збіцца:

  • Функцыя: значэнні, якія вяртаюцца, падаюць у useActionState і адображаюцца пад кнопкай. Неконтрольаванае адхыленне падае на межу адных бядаў Next, пакуль не будзе перакрытае.
  • createServerFn: практэса адхыляецца; яго можна перакрыць у onSubmit.
  • tRPC: у мутацыі наявны параметры isError; тая ж самая праця з каратэрызацыём.
  • JS выключаны. Адзін спосаб працаваў, а два іншыя — ні. гэта разліка ў кантракце, а не моральныя крытэрыя. Старонкі налашоўвання маркетынгу, якія павінны працаваць без JS, должны вікорыстоўваць Action-ы. Дашборды, якім важка без JS, можаць ігнораваць гэты аспект.

    Додатковыя патрабаванні за межамі схемы

    Адаптары зайнялі весь вечар, а не Zod.

    Форма паказання аднойчыні змянілася, пакуль не была стандартызавана як { error?: string }. tRPC вярнуўся да error.message; каратэрызуйце яго аднойчы. Не дазвольце трохкі разным формам прайсці да аблака пад кнопкай.

    Методы аўтэнтыфікацыі разлічваліся: Action-ы вікорыстоўвалі кукі з запітамі да дакументаў; tRPC — кантэкст прыемлівання процедуры; createServerFn — запіты з Start. Усе можаць быць правільнымі; толькі адна з іх ўособлівае “засылаюцца форма”.

    Часы апдэта кэша пасля успеху таксама збіліліся у тры разы: дзеянне updateTag("invites"), tRPC utils.invites.list.invalidate() — запуск анвалідэціі ладэра. Адна ж фраза, тры дыялекты.

    Калі выкорыстоўваць кожны з спосабаў

    Выбірайце Server Action, калі кантроль — гэта справжняя форма ў Next і значэнне JS-off яшчэ мае значэння. Абгорніце ў useActionState, вяртайце адказы пра аблыканні і уважайце, каб не перадаваць даныя за межы, калі хочаце інлайн-копію.

    Выбірайце createServerFn, калі вы ужо работаеце з Start і вызывачы — цэе тыпаваныя кліенты. Не пераходзіце на Start толькі для таго, каб атрымаць гэты хелпер для адной формы.

    Выбірайце tRPC, калі API прызначаны для мобайльных прыстроёў або партнераў. Не інсталюйце tRPC толькі для адправкі аднае электранаўпы.

    Не запускайце ўсе тры спосабы ў адной прыкладнай програме, калі толькі не вы мерзіце ўсё гэта.

    Строгія ліміты

    Функцыя JS-off на дзеяннях працюе толькі тады, калі полы ўсё-такі маюць рэальныя назвы. Стан, які існуе толькі ў React, не перазьявіцца пасля гэтага тэсту.

    У лабараторных умовах былі перакананы кількасць запускаў, тыпы дадзеных і рэзультаты адправкі з JS-off — а не TTI. Калі пасля вашага запаведзення кнопка нічога не робіць, калі JS выключаны, значы выбор бэкенду для гэтай старонкі некарэктны.

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

    Тры шляхі запраса, тры формы адзінакоў

    У форме запрасу ў налаштаваннях той жа некарэктны адрэс электронной пошты стварыў тры розныя інтэрфейсы: дзеяння падкрэслены за дапамогою useActionState, createServerFn выклікаў генерычную памятку пра адзінак, а tRPC заставіў дэталі Zod у error.data.zodError без прыналежнага карэльвання. Затрымка была аднойчынай (~40 мс). Проблема заключалася ў розніце падходаў у продукцыі.

    Кэшаваныя Invalid-заглувкі стараліся вымазаць ідэнтыфікат поля. Праўільны спосаб прыладзення — аднолькі мэйпар:

    function fieldErrors(err: unknown): Record<string, string> {
      if (err && typeof err === "object" && "issues" in err) {
        const issues = (err as { issues: { path: (string|number)[]; message: string }[] }).issues;
        return Object.fromEntries(issues.map((i) => [String(i.path[0]), i.message]));
      }
      return { form: "Could not send invite" };
    }
    

    Кожны бэкенд завершаецца гэтым мэйпарам пры выходзе на UI. Некарэктны адрэс электранейтавання завжды падкрэсляе яго. Заставіце дзеяння на старонцы наступных настройкаў, createServerFn — на старонцы запуску эксперымента, а tRPC ў мобільным працоўніку ужо функцыонуе. Кальколькі бэкенда ўсё гаразд, калі яны дзеляюць шыму і мэйпар; кальколькі фармата адзякаў — ні.

    Відтворэнне

    Адправіце порожній, nope, а потым правільны адрэс. Зафіксавайце JSON або стан дзеяння. Калі шлях да паведамлення разны, вы адправілі тры продукты.

    Прыклад, які працуе: адна шыма, тры запісы

    export const Invite = z.object({
      email: z.string().email(),
      role: z.enum(["admin", "member"]),
    });
    export type Invite = z.infer<typeof Invite>;
    

    Дзеяння сервера:

    "use server";
    export async function inviteAction(_prev: unknown, form: FormData) {
      const parsed = Invite.safeParse({
        email: form.get("email"),
        role: form.get("role"),
      });
      if (!parsed.success) return { errors: fieldErrors(parsed.error) };
      await sendInvite(parsed.data);
      return { errors: {} };
    }
    

    tRPC:

    invite: publicProcedure.input(Invite).mutation(({ input }) => sendInvite(input)),
    

    createServerFn:

    export const inviteFn = createServerFn({ method: "POST" })
      .validator(Invite.parse)
      .handler(({ data }) => sendInvite(data));
    

    Інтэрфейс завжды чытае errors.email. Бэкенды, якія не можаць стварыць гэты ключ, не выкорыстоўваюцца ў этой форме.

    Кансалтацыя для наступнага інжынера

    Схема — у адным файле. Мапер — у адным файле. Новыя способы перадачы даных дазволены толькі якшо ў іх назве ёсць гэты мапер. Паказанні памылак у відэа-паведамленнях не дазволены. Залейце настройкі invite на шлях Action, а mobile — на tRPC. Не ствараюце чэргавы спосаб перадачы даных толькі таму, што ў нарадзе гэтага тыдня хвалілі GraphQL.

    Каманды для сесій у лабараторыі

    Запісуйце версіі інструментаў прычымоў перш чым атрымваць рэзультаты:

    node -v
    pnpm exec tsc -v
    pnpm exec next --version
    

    Потым запускайце прыстрой:

    pnpm exec next dev
    

    Перайдзіце па шляхах /, /invoices, /invoices/1, /settings і зноў назад. Зберагучы логі DevTools. Адрабатайце пераканранне тыпаў:

    pnpm exec tsc --noEmit --pretty false
    echo $?
    

    Код выхідныя 0 ўоскрэсляе права на аналіз працы програмы, але не ўтверджуе якосць продукту. Зноў запрацаваце тэсты JS-on / JS-off / throw matrix на свайму апараце — тэмпература, RAM і вікна Chrome значнейшае паўтараюць часы выканання, чым незначныя змены ў фрэймворку.

    Запішыце на аднай лініі прыметку пра невялікі успех (Прабавана X, але ўсё роўна бачыцца Y), каб зошыт заставаўся доказам, а не проста інформацыйным матэрыялам.

    Дзеньнік неудач і чарт перагляду

    tRPC выкарыстоўваў error.data.zodError.fieldErrors.email[0]. Функцыі выкарыстоўвалі state.errors.email. Спачатку выклікаўся необработаны ZodError. Неабходна аднаковасць у фармаце errors.email: string; ўсё іншае — глюк у маперы.

    Чатверты маршрут fetch('/api/invite') з’явіўся раз і быў выдалены з каментарыем, які вяло да inviteAction. Коментары лепшыя, чым чатвертыя маперы.

    Перш чым дадаць транспорт:

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

    Заўважэння з рэальных умоў выкарыстоўвання

    Колькісць запрошэнняў збілася на тры бэкенды, таму што існуе троха кліентав: наступныя настройкі, пачатак эксперымента, мобайльныя прыстроі. Корыстнікі вводзяць адрэсу электронной пошты. Відсутнае падкрэсленне стварае заявкі тыпу “ўсё гаразд”, калі пазней працавнікі выкарыстоўваюць некарэктныя даныя. Следзіце за колькасцю выданых запрошэнняў у параўнананні з тымі, якія планаваліся. Продукт — гэта мапер; спосаб прынесення — гэта тэхнічная інфраструктура. Калі з’явіцца спрага да GraphQL, зноў адкройце тры варыянты адпаведных практычных проблем і адыходзьце.

    Ценнае для эксперыменту ёст табліца парабянаў, а не знак пераможця. З’явіце інфармацыю пра тое, які шлях застаецца працоўным без JS, які вяртаеія памылкі з ключамі полей без можлівасці адхілення праз мапер, і які ўжо падходзіць для кліентаў, якімі вы керуеце. Пераглядзіце гэтую табліцу ўсё час, калі хтось прапанавае “штоўна ўсё тое ж” канцэнтрацыю запрасоў.