Dlaczego działania serwera Next.js zmieniają grę!

May 07 2023
Nie wiem jak ty, ale za każdym razem, gdy ogłaszana jest nowa wersja Next.js, jestem bardzo podekscytowany, widząc, jakie nowe funkcje zostały wydane.

Nie wiem jak ty, ale za każdym razem, gdy ogłaszana jest nowa wersja Next.js, jestem bardzo podekscytowany, widząc, jakie nowe funkcje zostały wydane. Obecnie Vercel jest w ścisłej czołówce z Reactem w przenoszeniu jak największej ilości na serwer, co jest bardzo ekscytujące! W zeszłym tygodniu Vercel ogłosił wydanie Next.js 13.4 , które zawierało nowy paradygmat aktualizacji danych na serwerze o nazwie Server Actions.

Akcje serwera mają na celu umożliwienie aktualizacji danych po stronie serwera, zmniejszenie liczby JavaScript po stronie klienta i zapewnienie stopniowo udoskonalonych formularzy. W tym artykule zagłębimy się w podstawy akcji serwera, zbadamy, jak je tworzyć i wywoływać, a także omówimy różne ulepszenia i korzyści, jakie wnoszą do aplikacji Next.js.

ℹ️ Zwróć uwagę , że te przykłady zostały zaczerpnięte bezpośrednio z witryny dokumentacji Next.js. Zobacz poniżej przykład wieloetapowego kreatora formularzy.

Włączanie akcji serwera

Aby włączyć akcje serwera w projekcie Next.js, musisz ustawić serverActionsflagę eksperymentalną w swoim next.config.jspliku:

const nextConfig = {
  experimental: {
    serverActions: true,
  },
};

Tworzenie akcji serwera jest tak proste, jak utworzenie funkcji asynchronicznej z dyrektywą „użyj serwera” na górze treści funkcji. Funkcja powinna mieć serializowalne argumenty i możliwą do serializacji wartość zwracaną, w oparciu o protokół React Server Components. Oto przykład:

async function myAction() {
  "use server"
  // Your server action logic here
}

Możesz także użyć dyrektywy najwyższego poziomu „użyj serwera” na początku pliku, co jest przydatne, jeśli masz jeden plik, który eksportuje wiele działań serwera, i jest wymagane, jeśli importujesz działanie serwera w komponencie klienta.

"use server"

export async function myAction() {
  // Your server action logic here
}

Według strony internetowej z dokumentacją Next.js istnieje kilka sposobów wywoływania akcji serwera:

  1. Używanie actionrekwizytu: rekwizyt Reacta actionumożliwia wywołanie akcji serwera na <form>elemencie.
  2. export default function AddToCart({ productId }) {
      async function addItem(data) {
        'use server'
        const cartId = cookies().get('cartId')?.value;
        await saveToDb({ cartId, data });
      }
    
      return (
        <form action={addItem}>
          <button type="submit">Add to Cart</button>
        </form>
      )
    }
    

    export default function Form() {
      async function handleSubmit() {
        "use server"
        // Your server action logic here
      }
    
      async function submitImage() {
        "use server"
        // Your server action logic here
      }
      
      return (
        <form action={handleSubmit}>
          <input type="text" name="name" />
          <input type="image" formAction={submitImage} />
          <button type="submit">Submit</button>
        </form>
      )
    }
    

'use client'
import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './_actions.js'

export function Thread({ messages }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state, newMessage) => [...state, { message: newMessage, sending: true }],
  )

  const formRef = useRef();
  
  return (
    <div>
      {optimisticMessages.map((m) => (
        <div>
          {m.message}
          {m.sending ? 'Sending...' : ''}
        </div>
      ))}
      <form
        action={async (formData) => {
          const message = formData.get('message')
          formRef.current.reset()
          addOptimisticMessage(message)
          await send(message)
        }}
        ref={formRef}
      >
        <input type="text" name="message" />
      </form>
    </div>
  )
}

  1. Jeśli akcja klienta zostanie przekazana do <form>, formularz pozostaje interaktywny, ale akcja jest umieszczana w kolejce do czasu uwodnienia formularza. Priorytetem jest <form>selektywne nawodnienie, więc dzieje się to szybko.
  2. // src/components/MyForm.js
    'use client'
    import { useState } from 'react'
    import { handleSubmit } from './actions.js'
    
    export default function MyForm({ myAction }) {
      const [input, setInput] = useState();
      return (
        <form action={handleSubmit} onChange={(e) => setInput(e.target.value)}>
          ...
        </form>
      )
    }
    

Połączmy to wszystko razem, aby utworzyć przykładowy przepływ rejestracji kreatora przy użyciu akcji serwera w celu zapisania danych na każdym etapie przepływu.

Najpierw dodamy stronę:

// app/register/page.tsx
import RegistrationForm from 'src/components/RegistrationForm'

export default function Register() {
  return (
    <div>
      <h1>Registration</h1>
      <RegistrationForm />
    </div>
  )
}


// components/RegistrationForm.tsx
'use client'
import React, { useState } from 'react';
import {
  savePersonalInformation,
  saveContactInformation,
  saveAccountDetails
} from '../../actions'
import PersonalInformationStep from './PersonalInformationStep'
import ContactInformationStep from './ContactInformationStep'
import AccountDetailsStep from './AccountDetailsStep'

const WIZARD_STEPS = [
  {
    Component: PersonalInformationStep,
    action: savePersonalInformation,
  },
  {
    Component: ContactInformationStep,
    action: saveContactInformation,
  },
  {
    Component: AccountDetailsStep,
    action: saveAccountDetails,
  },
]

export default function RegistrationForm() {
  const [step, setStep] = useState(0);
  const { Component, action } = WIZARD_STEPS[step]

  const handleSubmit = async (data: FormData) => {
    await action(data);
    if (step < WIZARD_STEPS.length - 1) {
      setStep((currentStep) => currentStep + 1)
    } else {
      // Submit multi-step wizard form and navigate
    }
  }

  return (
    <Component onSubmit={handleSubmit} />
  )
}

// actions/index.ts
'use server'
import { cookies } from 'next/headers'
import prisma from 'prisma'
import { hashPassword } from 'src/utils/auth'

export async function savePersonalInformation(data: FormData) {
  const user = await prisma.user.create({
    data: {
      firstName: data.get('firstName').toString(),
      lastName: data.get('lastName').toString(),
    },
  })
  cookies().set('userId', user.id)
}

export async function saveContactInformation(data: FormData) {
  // Save contact information to the database
  const id = cookies().get('userId')?.value
  await prisma.user.update({
    where: { id },
    data: {
      email: data.get('email').toString(),
      phone: data.get('phone').toString(),
    },
  })
}

export async function saveAccountDetails(data: FormData) {
  // Save account details
  const id = cookies().get('userId')?.value
  const password = await hashPassword(
    data.get('password').toString()
  );
  await prisma.user.update({
    where: { id },
    data: {
      username: data.get('username').toString(),
      password,
    },
  })
}

// components/WizardStep.tsx
import React from 'react'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'

type Props = {
  title: string;
  onSubmit: (data: FormData) => Promise<void>
  children: React.ReactNode
}

export default function FormStep({ title, onSubmit, children }: Props) {
  const { pending } = useFormStatus();
  return (
    <form
      action={async (formData) => {
        await onSubmit(formData);
      }}
    >
      <h2>{title}</h2>
      {children}
      <button
        type="submit"
        className={pending ? 'button-submitting' : 'button'}
      >
        Next
      </button>
    </form>
  )
}

// ./PersonalInformationStep.tsx
import React from 'react'
import WizardStep from '../WizardStep'

type Props = {
  onSubmit: (data: FormData) => Promise<void>
}

export default function PersonalInformationStep({ onSubmit }: Props) {
  return (
    <WizardStep title="Personal Information" onSubmit={onSubmit}>
      <label htmlFor="firstName">First Name:</label>
      <input type="text" id="firstName" name="firstName" required />
      <label htmlFor="lastName">Last Name:</label>
      <input type="text" id="lastName" name="lastName" required />
    </WizardStep>
  )
}

Dlaczego działania serwera zmieniają grę

Zanim zakończymy, poświęćmy chwilę na omówienie, dlaczego akcje serwera Next.js są tak potężnym dodatkiem do ekosystemu Next.js i jak mogą na nowo zdefiniować najlepsze praktyki tworzenia stron internetowych.

  1. Zredukowany JavaScript po stronie klienta: Next.js i React zmierzają w kierunku minimalizacji pakietu po stronie klienta poprzez przeniesienie logiki na serwer. Next.js umożliwił nam przeniesienie dużej części kodu na serwer, ale nadal głównie budowaliśmy formularze na kliencie. Zmienia się to wraz z akcjami serwera!
  2. Lepsze wrażenia użytkownika: dzięki funkcjom, takim jak optymistyczne aktualizacje i progresywne ulepszenia, programiści mogą tworzyć aplikacje, które są bardziej responsywne i płynne. Użytkownicy nie muszą już czekać na odpowiedź serwera, aby zobaczyć efekty swoich działań, a formularze pozostają interaktywne nawet bez JavaScript, co prowadzi do lepszego ogólnego doświadczenia użytkownika, nawet zanim klient się zużyje.
  3. Doskonałe doświadczenie programisty: Akcje serwera ułatwiają przenoszenie aktualizacji danych na serwer, co w przeszłości wymagałoby interfejsu API. Interfejsy API udostępniane przez Next.js i React bardzo ułatwiają pracę.

Podsumowując, Next.js Server Actions to rewolucyjna funkcja, która może zrewolucjonizować sposób, w jaki twórcy stron internetowych podchodzą do mutacji danych po stronie serwera i obsługi formularzy. Oferując bardziej responsywne, skalowalne i usprawnione środowisko programistyczne, Server Actions może stać się niezbędnym narzędziem do nowoczesnego tworzenia stron internetowych.