Server-Funktionen

Was sind Server Functions?

Server Functions ermöglichen es Ihnen, Logik zu definieren, die fast überall (auch auf dem Client) aufgerufen werden kann, aber nur auf dem Server ausgeführt wird. Tatsächlich unterscheiden sie sich nicht wesentlich von API Routes, jedoch mit einigen wichtigen Unterschieden

  • Sie haben keine stabile öffentliche URL.
  • Sie können von überall in Ihrer Anwendung aufgerufen werden, einschließlich Loader, Hooks, Komponenten, Server-Routen usw.

Sie ähneln jedoch regulären API Routes insofern, als dass

  • Sie Zugriff auf den Request-Kontext haben, was Ihnen erlaubt, Header zu lesen, Cookies zu setzen und mehr
  • Sie auf sensible Informationen wie Umgebungsvariablen zugreifen können, ohne sie dem Client preiszugeben
  • Sie zur Ausführung jeglicher serverseitiger Logik verwendet werden können, wie z. B. das Abrufen von Daten aus einer Datenbank, das Senden von E-Mails oder die Interaktion mit anderen Diensten
  • Sie jeden beliebigen Wert zurückgeben können, einschließlich primitiver Werte, JSON-serialisierbarer Objekte und sogar roher Response-Objekte
  • Sie Fehler auslösen können, einschließlich Weiterleitungen und NotFounds, die automatisch vom Router behandelt werden können

Wie unterscheiden sich Server Functions von "React Server Functions"?

  • TanStack Server Functions sind nicht an ein bestimmtes Frontend-Framework gebunden und können mit jedem Frontend-Framework oder auch ohne eines verwendet werden.
  • TanStack Server Functions basieren auf Standard-HTTP-Anfragen und können beliebig oft aufgerufen werden, ohne unter serverseitigen Ausführungsengpässen zu leiden.

Wie funktionieren sie?

Server Functions können überall in Ihrer Anwendung definiert werden, müssen aber auf der obersten Ebene einer Datei definiert sein. Sie können in Ihrer gesamten Anwendung aufgerufen werden, einschließlich Loader, Hooks usw. Traditionell ist dieses Muster als Remote Procedure Call (RPC) bekannt, aber aufgrund der isomorphen Natur dieser Funktionen bezeichnen wir sie als Server Functions.

  • Auf dem Server-Bundle bleibt die Logik der Server Functions unverändert. Es muss nichts unternommen werden, da sie sich bereits am richtigen Ort befindet.
  • Auf dem Client werden Server Functions entfernt; sie existieren nur auf dem Server. Jeder Aufruf einer Server Function auf dem Client wird durch eine fetch-Anfrage an den Server ersetzt, um die Server Function auszuführen und die Antwort an den Client zurückzusenden.

Server Function Middleware

Server Functions können Middleware verwenden, um Logik, Kontext, gemeinsame Operationen, Voraussetzungen und vieles mehr zu teilen. Um mehr über die Middleware von Server Functions zu erfahren, lesen Sie bitte das Middleware-Handbuch.

Server Functions definieren

Wir möchten dem tRPC-Team für die Inspiration zum Design der Server Functions von TanStack Start und für die Unterstützung bei der Implementierung danken. Wir lieben (und empfehlen) die Verwendung von tRPC für API Routes so sehr, dass wir darauf bestanden haben, dass Server Functions die gleiche erstklassige Behandlung und Entwicklererfahrung erhalten. Vielen Dank!

Server Functions werden mit der Funktion createServerFn aus dem Paket @tanstack/react-start definiert. Diese Funktion nimmt ein optionales options-Argument für die Angabe von Konfigurationen wie HTTP-Methode und Antworttyp entgegen und ermöglicht es Ihnen, von dem Ergebnis abzuleiten, um Dinge wie den Body der Server Function, Eingabevalidierung, Middleware usw. zu definieren. Hier ist ein einfaches Beispiel

tsx
// getServerTime.ts
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn().handler(async () => {
  // Wait for 1 second
  await new Promise((resolve) => setTimeout(resolve, 1000))
  // Return the current time
  return new Date().toISOString()
})
// getServerTime.ts
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn().handler(async () => {
  // Wait for 1 second
  await new Promise((resolve) => setTimeout(resolve, 1000))
  // Return the current time
  return new Date().toISOString()
})

Konfigurationsoptionen

Bei der Erstellung einer Server Function können Sie Konfigurationsoptionen angeben, um ihr Verhalten anzupassen

tsx
import { createServerFn } from '@tanstack/react-start'

export const getData = createServerFn({
  method: 'GET', // HTTP method to use
  response: 'data', // Response handling mode
}).handler(async () => {
  // Function implementation
})
import { createServerFn } from '@tanstack/react-start'

export const getData = createServerFn({
  method: 'GET', // HTTP method to use
  response: 'data', // Response handling mode
}).handler(async () => {
  // Function implementation
})

Verfügbare Optionen

method

Gibt die HTTP-Methode für die Server Function-Anfrage an

tsx
method?: 'GET' | 'POST'
method?: 'GET' | 'POST'

Standardmäßig verwenden Server Functions GET, wenn nicht anders angegeben.

response

Steuert, wie Antworten verarbeitet und zurückgegeben werden

tsx
response?: 'data' | 'full' | 'raw'
response?: 'data' | 'full' | 'raw'
  • 'data' (Standard): Parst automatisch JSON-Antworten und gibt nur die Daten zurück
  • 'full': Gibt ein Response-Objekt mit Ergebnisdaten, Fehlerinformationen und Kontext zurück
  • 'raw': Gibt das rohe Response-Objekt direkt zurück, was Streaming-Antworten und benutzerdefinierte Header ermöglicht

Wo kann ich Server Functions aufrufen?

  • Aus serverseitigem Code
  • Aus clientseitigem Code
  • Aus anderen Server Functions

Parameter akzeptieren

Server Functions akzeptieren einen einzelnen Parameter, der eine Vielzahl von Typen haben kann

  • Standard-JavaScript-Typen
    • string
    • number
    • boolean
    • null
    • Array
    • Object
  • FormData
  • ReadableStream (von einem der oben genannten Typen)
  • Promise (von einem der oben genannten Typen)

Hier ist ein Beispiel für eine Server Function, die einen einfachen String-Parameter akzeptiert

tsx
import { createServerFn } from '@tanstack/react-start'

export const greet = createServerFn({
  method: 'GET',
})
  .validator((data: string) => data)
  .handler(async (ctx) => {
    return `Hello, ${ctx.data}!`
  })

greet({
  data: 'John',
})
import { createServerFn } from '@tanstack/react-start'

export const greet = createServerFn({
  method: 'GET',
})
  .validator((data: string) => data)
  .handler(async (ctx) => {
    return `Hello, ${ctx.data}!`
  })

greet({
  data: 'John',
})

Laufende Eingabevalidierung / Typsicherheit

Server Functions können so konfiguriert werden, dass sie ihre Eingabedaten zur Laufzeit validieren und gleichzeitig Typsicherheit hinzufügen. Dies ist nützlich, um sicherzustellen, dass die Eingabe den richtigen Typ hat, bevor die Server Function ausgeführt wird, und um freundlichere Fehlermeldungen bereitzustellen.

Dies geschieht mit der Methode validator. Sie akzeptiert jede Eingabe, die an die Server Function übergeben wird. Der Wert (und Typ), den Sie aus dieser Funktion zurückgeben, wird die Eingabe sein, die an den eigentlichen Handler der Server Function übergeben wird.

Validatoren integrieren sich auch nahtlos in externe Validatoren, wenn Sie etwas wie Zod verwenden möchten.

Grundlegende Validierung

Hier ist ein einfaches Beispiel für eine Server Function, die den Eingabeparameter validiert

tsx
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown): Person => {
    if (typeof person !== 'object' || person === null) {
      throw new Error('Person must be an object')
    }

    if ('name' in person && typeof person.name !== 'string') {
      throw new Error('Person.name must be a string')
    }

    return person as Person
  })
  .handler(async ({ data }) => {
    return `Hello, ${data.name}!`
  })
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown): Person => {
    if (typeof person !== 'object' || person === null) {
      throw new Error('Person must be an object')
    }

    if ('name' in person && typeof person.name !== 'string') {
      throw new Error('Person.name must be a string')
    }

    return person as Person
  })
  .handler(async ({ data }) => {
    return `Hello, ${data.name}!`
  })

Verwendung einer Validierungsbibliothek

Validierungsbibliotheken wie Zod können wie folgt verwendet werden

tsx
import { createServerFn } from '@tanstack/react-start'

import { z } from 'zod'

const Person = z.object({
  name: z.string(),
})

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown) => {
    return Person.parse(person)
  })
  .handler(async (ctx) => {
    return `Hello, ${ctx.data.name}!`
  })

greet({
  data: {
    name: 'John',
  },
})
import { createServerFn } from '@tanstack/react-start'

import { z } from 'zod'

const Person = z.object({
  name: z.string(),
})

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown) => {
    return Person.parse(person)
  })
  .handler(async (ctx) => {
    return `Hello, ${ctx.data.name}!`
  })

greet({
  data: {
    name: 'John',
  },
})

Typsicherheit

Da Server Functions die Netzwerkgrenze überschreiten, ist es wichtig sicherzustellen, dass die an sie übergebenen Daten nicht nur den richtigen Typ haben, sondern auch zur Laufzeit validiert werden. Dies ist besonders wichtig bei der Verarbeitung von Benutzereingaben, da diese unvorhersehbar sein können. Um Entwickler zu ermutigen, ihre I/O-Daten zu validieren, sind Typen von der Validierung abhängig. Der Rückgabetyp der validator-Funktion ist die Eingabe für den Handler der Server Function.

tsx
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown): Person => {
    if (typeof person !== 'object' || person === null) {
      throw new Error('Person must be an object')
    }

    if ('name' in person && typeof person.name !== 'string') {
      throw new Error('Person.name must be a string')
    }

    return person as Person
  })
  .handler(
    async ({
      data, // Person
    }) => {
      return `Hello, ${data.name}!`
    },
  )

function test() {
  greet({ data: { name: 'John' } }) // OK
  greet({ data: { name: 123 } }) // Error: Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.
}
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((person: unknown): Person => {
    if (typeof person !== 'object' || person === null) {
      throw new Error('Person must be an object')
    }

    if ('name' in person && typeof person.name !== 'string') {
      throw new Error('Person.name must be a string')
    }

    return person as Person
  })
  .handler(
    async ({
      data, // Person
    }) => {
      return `Hello, ${data.name}!`
    },
  )

function test() {
  greet({ data: { name: 'John' } }) // OK
  greet({ data: { name: 123 } }) // Error: Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.
}

Inferenz

Server Functions inferieren ihre Eingabe- und Ausgabetypen basierend auf der Eingabe des validator und dem Rückgabewert der handler-Funktionen. Tatsächlich kann der von Ihnen definierte validator eigene separate Eingabe-/Ausgabetypen haben, was nützlich sein kann, wenn Ihr Validator Transformationen an den Eingabedaten durchführt.

Um dies zu veranschaulichen, werfen wir einen Blick auf ein Beispiel mit der zod-Validierungsbibliothek

tsx
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

const transactionSchema = z.object({
  amount: z.string().transform((val) => parseInt(val, 10)),
})

const createTransaction = createServerFn()
  .validator(transactionSchema)
  .handler(({ data }) => {
    return data.amount // Returns a number
  })

createTransaction({
  data: {
    amount: '123', // Accepts a string
  },
})
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

const transactionSchema = z.object({
  amount: z.string().transform((val) => parseInt(val, 10)),
})

const createTransaction = createServerFn()
  .validator(transactionSchema)
  .handler(({ data }) => {
    return data.amount // Returns a number
  })

createTransaction({
  data: {
    amount: '123', // Accepts a string
  },
})

Nicht validierte Inferenz

Obwohl wir dringend empfehlen, eine Validierungsbibliothek zur Validierung Ihrer Netz-I/O-Daten zu verwenden, möchten Sie vielleicht aus irgendeinem Grund Ihre Daten nicht validieren, aber dennoch Typsicherheit haben. Um dies zu tun, stellen Sie Typinformationen für die Server Function über eine Identitätsfunktion als validator bereit, die die Eingabe und/oder Ausgabe auf die richtigen Typen typisiert

tsx
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((d: Person) => d)
  .handler(async (ctx) => {
    return `Hello, ${ctx.data.name}!`
  })

greet({
  data: {
    name: 'John',
  },
})
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
}

export const greet = createServerFn({ method: 'GET' })
  .validator((d: Person) => d)
  .handler(async (ctx) => {
    return `Hello, ${ctx.data.name}!`
  })

greet({
  data: {
    name: 'John',
  },
})

JSON-Parameter

Server Functions können JSON-serialisierbare Objekte als Parameter akzeptieren. Dies ist nützlich für die Übergabe komplexer Datenstrukturen an den Server

tsx
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
  age: number
}

export const greet = createServerFn({ method: 'GET' })
  .validator((data: Person) => data)
  .handler(async ({ data }) => {
    return `Hello, ${data.name}! You are ${data.age} years old.`
  })

greet({
  data: {
    name: 'John',
    age: 34,
  },
})
import { createServerFn } from '@tanstack/react-start'

type Person = {
  name: string
  age: number
}

export const greet = createServerFn({ method: 'GET' })
  .validator((data: Person) => data)
  .handler(async ({ data }) => {
    return `Hello, ${data.name}! You are ${data.age} years old.`
  })

greet({
  data: {
    name: 'John',
    age: 34,
  },
})

FormData-Parameter

Server Functions können FormData-Objekte als Parameter akzeptieren

tsx
import { createServerFn } from '@tanstack/react-start'

export const greetUser = createServerFn({ method: 'POST' })
  .validator((data) => {
    if (!(data instanceof FormData)) {
      throw new Error('Invalid form data')
    }
    const name = data.get('name')
    const age = data.get('age')

    if (!name || !age) {
      throw new Error('Name and age are required')
    }

    return {
      name: name.toString(),
      age: parseInt(age.toString(), 10),
    }
  })
  .handler(async ({ data: { name, age } }) => {
    return `Hello, ${name}! You are ${age} years old.`
  })

// Usage
function Test() {
  return (
    <form
      onSubmit={async (event) => {
        event.preventDefault()
        const formData = new FormData(event.currentTarget)
        const response = await greetUser({ data: formData })
        console.log(response)
      }}
    >
      <input name="name" />
      <input name="age" />
      <button type="submit">Submit</button>
    </form>
  )
}
import { createServerFn } from '@tanstack/react-start'

export const greetUser = createServerFn({ method: 'POST' })
  .validator((data) => {
    if (!(data instanceof FormData)) {
      throw new Error('Invalid form data')
    }
    const name = data.get('name')
    const age = data.get('age')

    if (!name || !age) {
      throw new Error('Name and age are required')
    }

    return {
      name: name.toString(),
      age: parseInt(age.toString(), 10),
    }
  })
  .handler(async ({ data: { name, age } }) => {
    return `Hello, ${name}! You are ${age} years old.`
  })

// Usage
function Test() {
  return (
    <form
      onSubmit={async (event) => {
        event.preventDefault()
        const formData = new FormData(event.currentTarget)
        const response = await greetUser({ data: formData })
        console.log(response)
      }}
    >
      <input name="name" />
      <input name="age" />
      <button type="submit">Submit</button>
    </form>
  )
}

Server Function Kontext

Zusätzlich zu dem einzelnen Parameter, den Server Functions akzeptieren, können Sie auch auf den serverseitigen Request-Kontext innerhalb jeder Server Function zugreifen, indem Sie Hilfsmittel aus @tanstack/react-start/server verwenden. Unter der Haube verwenden wir das h3-Paket von Unjs, um plattformübergreifende HTTP-Anfragen durchzuführen.

Es gibt viele Kontextfunktionen, die Ihnen zur Verfügung stehen, z. B. für

  • Zugriff auf den Request-Kontext
  • Zugriff/Setzen von Headern
  • Zugriff/Setzen von Sitzungen/Cookies
  • Setzen von Antwort-Statuscodes und Statusmeldungen
  • Umgang mit Multi-Part-Formulardaten
  • Lesen/Setzen von benutzerdefinierten Server-Kontexteigenschaften

Eine vollständige Liste der verfügbaren Kontextfunktionen finden Sie unter allen verfügbaren h3-Methoden oder inspizieren Sie den @tanstack/start-server-core-Quellcode.

Für den Anfang hier ein paar Beispiele

Zugriff auf den Request-Kontext

Verwenden wir die Funktion getWebRequest, um auf den Request selbst innerhalb einer Server Function zuzugreifen

tsx
import { createServerFn } from '@tanstack/react-start'
import { getWebRequest } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    const request = getWebRequest()

    console.log(request.method) // GET

    console.log(request.headers.get('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
  },
)
import { createServerFn } from '@tanstack/react-start'
import { getWebRequest } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    const request = getWebRequest()

    console.log(request.method) // GET

    console.log(request.headers.get('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
  },
)

Zugriff auf Header

Verwenden Sie die Funktion getHeaders, um innerhalb einer Server Function auf alle Header zuzugreifen

tsx
import { createServerFn } from '@tanstack/react-start'
import { getHeaders } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log(getHeaders())
    // {
    //   "accept": "*/*",
    //   "accept-encoding": "gzip, deflate, br",
    //   "accept-language": "en-US,en;q=0.9",
    //   "connection": "keep-alive",
    //   "host": "localhost:3000",
    //   ...
    // }
  },
)
import { createServerFn } from '@tanstack/react-start'
import { getHeaders } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log(getHeaders())
    // {
    //   "accept": "*/*",
    //   "accept-encoding": "gzip, deflate, br",
    //   "accept-language": "en-US,en;q=0.9",
    //   "connection": "keep-alive",
    //   "host": "localhost:3000",
    //   ...
    // }
  },
)

Sie können auch auf einzelne Header über die Funktion getHeader zugreifen

tsx
import { createServerFn } from '@tanstack/react-start'
import { getHeader } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log(getHeader('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
  },
)
import { createServerFn } from '@tanstack/react-start'
import { getHeader } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    console.log(getHeader('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
  },
)

Werte zurückgeben

Server Functions können verschiedene Arten von Rückgabewerten haben

  • Primitive
  • JSON-serialisierbare Objekte
  • redirect-Fehler (können auch ausgelöst werden)
  • notFound-Fehler (können auch ausgelöst werden)
  • Rohe Response-Objekte

Primitive und JSON zurückgeben

Um beliebige primitive oder JSON-serialisierbare Objekte zurückzugeben, geben Sie einfach den Wert aus der Server Function zurück

tsx
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    return new Date().toISOString()
  },
)

export const getServerData = createServerFn({ method: 'GET' }).handler(
  async () => {
    return {
      message: 'Hello, World!',
    }
  },
)
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    return new Date().toISOString()
  },
)

export const getServerData = createServerFn({ method: 'GET' }).handler(
  async () => {
    return {
      message: 'Hello, World!',
    }
  },
)

Standardmäßig gehen Server Functions davon aus, dass jedes nicht-Response-Objekt entweder ein primitives oder ein JSON-serialisierbares Objekt ist.

Antworten mit benutzerdefinierten Headern

Um mit benutzerdefinierten Headern zu antworten, können Sie die Funktion setHeader verwenden

tsx
import { createServerFn } from '@tanstack/react-start'
import { setHeader } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setHeader('X-Custom-Header', 'value')
    return new Date().toISOString()
  },
)
import { createServerFn } from '@tanstack/react-start'
import { setHeader } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setHeader('X-Custom-Header', 'value')
    return new Date().toISOString()
  },
)

Antworten mit benutzerdefinierten Statuscodes

Um mit einem benutzerdefinierten Statuscode zu antworten, können Sie die Funktion setResponseStatus verwenden

tsx
import { createServerFn } from '@tanstack/react-start'
import { setResponseStatus } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setResponseStatus(201)
    return new Date().toISOString()
  },
)
import { createServerFn } from '@tanstack/react-start'
import { setResponseStatus } from '@tanstack/react-start/server'

export const getServerTime = createServerFn({ method: 'GET' }).handler(
  async () => {
    setResponseStatus(201)
    return new Date().toISOString()
  },
)

Rohe Response-Objekte zurückgeben

Um ein rohes Response-Objekt zurückzugeben, geben Sie ein Response-Objekt aus der Server Function zurück und setzen response: 'raw'

tsx
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn({
  method: 'GET',
  response: 'raw',
}).handler(async () => {
  // Read a file from s3
  return fetch('https://example.com/time.txt')
})
import { createServerFn } from '@tanstack/react-start'

export const getServerTime = createServerFn({
  method: 'GET',
  response: 'raw',
}).handler(async () => {
  // Read a file from s3
  return fetch('https://example.com/time.txt')
})

Die Option response: 'raw' ermöglicht auch Streaming-Antworten und andere Dinge

tsx
import { createServerFn } from '@tanstack/react-start'

export const streamEvents = createServerFn({
  method: 'GET',
  response: 'raw',
}).handler(async ({ signal }) => {
  // Create a ReadableStream to send chunks of data
  const stream = new ReadableStream({
    async start(controller) {
      // Send initial response immediately
      controller.enqueue(new TextEncoder().encode('Connection established\n'))

      let count = 0
      const interval = setInterval(() => {
        // Check if the client disconnected
        if (signal.aborted) {
          clearInterval(interval)
          controller.close()
          return
        }

        // Send a data chunk
        controller.enqueue(
          new TextEncoder().encode(
            `Event ${++count}: ${new Date().toISOString()}\n`,
          ),
        )

        // End after 10 events
        if (count >= 10) {
          clearInterval(interval)
          controller.close()
        }
      }, 1000)

      // Ensure we clean up if the request is aborted
      signal.addEventListener('abort', () => {
        clearInterval(interval)
        controller.close()
      })
    },
  })

  // Return a streaming response
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  })
})
import { createServerFn } from '@tanstack/react-start'

export const streamEvents = createServerFn({
  method: 'GET',
  response: 'raw',
}).handler(async ({ signal }) => {
  // Create a ReadableStream to send chunks of data
  const stream = new ReadableStream({
    async start(controller) {
      // Send initial response immediately
      controller.enqueue(new TextEncoder().encode('Connection established\n'))

      let count = 0
      const interval = setInterval(() => {
        // Check if the client disconnected
        if (signal.aborted) {
          clearInterval(interval)
          controller.close()
          return
        }

        // Send a data chunk
        controller.enqueue(
          new TextEncoder().encode(
            `Event ${++count}: ${new Date().toISOString()}\n`,
          ),
        )

        // End after 10 events
        if (count >= 10) {
          clearInterval(interval)
          controller.close()
        }
      }, 1000)

      // Ensure we clean up if the request is aborted
      signal.addEventListener('abort', () => {
        clearInterval(interval)
        controller.close()
      })
    },
  })

  // Return a streaming response
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  })
})

Die Option response: 'raw' ist besonders nützlich für

  • Streaming-APIs, bei denen Daten inkrementell gesendet werden
  • Server-Sent Events
  • Long-Polling-Antworten
  • Benutzerdefinierte Content-Typen und Binärdaten

Fehler auslösen

Neben speziellen redirect- und notFound-Fehlern können Server Functions beliebige benutzerdefinierte Fehler auslösen. Diese Fehler werden serialisiert und als JSON-Antwort mit einem 500er Statuscode an den Client gesendet.

tsx
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  throw new Error('Something went wrong!')
})

// Usage
function Test() {
  try {
    await doStuff()
  } catch (error) {
    console.error(error)
    // {
    //   message: "Something went wrong!",
    //   stack: "Error: Something went wrong!\n    at doStuff (file:///path/to/file.ts:3:3)"
    // }
  }
}
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  throw new Error('Something went wrong!')
})

// Usage
function Test() {
  try {
    await doStuff()
  } catch (error) {
    console.error(error)
    // {
    //   message: "Something went wrong!",
    //   stack: "Error: Something went wrong!\n    at doStuff (file:///path/to/file.ts:3:3)"
    // }
  }
}

Abbruch

Auf dem Client können Server Function-Aufrufe über ein AbortSignal abgebrochen werden. Auf dem Server benachrichtigt ein AbortSignal, wenn die Anfrage vor Abschluss der Ausführung geschlossen wurde.

tsx
import { createServerFn } from '@tanstack/react-start'

export const abortableServerFn = createServerFn().handler(
  async ({ signal }) => {
    return new Promise<string>((resolve, reject) => {
      if (signal.aborted) {
        return reject(new Error('Aborted before start'))
      }
      const timerId = setTimeout(() => {
        console.log('server function finished')
        resolve('server function result')
      }, 1000)
      const onAbort = () => {
        clearTimeout(timerId)
        console.log('server function aborted')
        reject(new Error('Aborted'))
      }
      signal.addEventListener('abort', onAbort, { once: true })
    })
  },
)

// Usage
function Test() {
  const controller = new AbortController()
  const serverFnPromise = abortableServerFn({
    signal: controller.signal,
  })
  await new Promise((resolve) => setTimeout(resolve, 500))
  controller.abort()
  try {
    const serverFnResult = await serverFnPromise
    console.log(serverFnResult) // should never get here
  } catch (error) {
    console.error(error) // "signal is aborted without reason"
  }
}
import { createServerFn } from '@tanstack/react-start'

export const abortableServerFn = createServerFn().handler(
  async ({ signal }) => {
    return new Promise<string>((resolve, reject) => {
      if (signal.aborted) {
        return reject(new Error('Aborted before start'))
      }
      const timerId = setTimeout(() => {
        console.log('server function finished')
        resolve('server function result')
      }, 1000)
      const onAbort = () => {
        clearTimeout(timerId)
        console.log('server function aborted')
        reject(new Error('Aborted'))
      }
      signal.addEventListener('abort', onAbort, { once: true })
    })
  },
)

// Usage
function Test() {
  const controller = new AbortController()
  const serverFnPromise = abortableServerFn({
    signal: controller.signal,
  })
  await new Promise((resolve) => setTimeout(resolve, 500))
  controller.abort()
  try {
    const serverFnResult = await serverFnPromise
    console.log(serverFnResult) // should never get here
  } catch (error) {
    console.error(error) // "signal is aborted without reason"
  }
}

Server Functions innerhalb von Route-Lifecycles aufrufen

Server Functions können normal aus Route-loadern, beforeLoads oder anderen vom Router gesteuerten APIs aufgerufen werden. Diese APIs sind dafür ausgestattet, Fehler, Weiterleitungen und NotFounds, die von Server Functions ausgelöst werden, automatisch zu behandeln.

tsx
import { getServerTime } from './getServerTime'

export const Route = createFileRoute('/time')({
  loader: async () => {
    const time = await getServerTime()

    return {
      time,
    }
  },
})
import { getServerTime } from './getServerTime'

export const Route = createFileRoute('/time')({
  loader: async () => {
    const time = await getServerTime()

    return {
      time,
    }
  },
})

Server Functions aus Hooks und Komponenten aufrufen

Server Functions können redirect- oder notFound-Fehler auslösen, und obwohl nicht zwingend erforderlich, wird empfohlen, diese Fehler abzufangen und entsprechend zu behandeln. Um dies zu erleichtern, exportiert das Paket @tanstack/react-start einen Hook namens useServerFn, der verwendet werden kann, um Server Functions an Komponenten und Hooks zu binden

tsx
import { useServerFn } from '@tanstack/react-start'
import { useQuery } from '@tanstack/react-query'
import { getServerTime } from './getServerTime'

export function Time() {
  const getTime = useServerFn(getServerTime)

  const timeQuery = useQuery({
    queryKey: 'time',
    queryFn: () => getTime(),
  })
}
import { useServerFn } from '@tanstack/react-start'
import { useQuery } from '@tanstack/react-query'
import { getServerTime } from './getServerTime'

export function Time() {
  const getTime = useServerFn(getServerTime)

  const timeQuery = useQuery({
    queryKey: 'time',
    queryFn: () => getTime(),
  })
}

Server Functions überall sonst aufrufen

Bei der Verwendung von Server Functions beachten Sie, dass Weiterleitungen und NotFounds, die sie auslösen, nur automatisch behandelt werden, wenn sie aufgerufen werden von

  • Route-Lebenszyklen
  • Komponenten, die den useServerFn-Hook verwenden

Für andere Einsatzorte müssen Sie diese Fälle manuell behandeln.

Weiterleitungen

Server Functions können einen redirect-Fehler auslösen, um den Benutzer zu einer anderen URL weiterzuleiten. Dies ist nützlich für die Behandlung von Authentifizierung, Autorisierung oder anderen Szenarien, bei denen Sie den Benutzer auf eine andere Seite weiterleiten müssen.

  • Während SSR werden Weiterleitungen durch das Senden einer 302-Antwort an den Client mit dem neuen Ort behandelt
  • Auf dem Client werden Weiterleitungen vom Router automatisch aus einem Route-Lifecycle oder einer Komponente, die den Hook useServerFn verwendet, behandelt. Wenn Sie eine Server Function von einem anderen Ort aus aufrufen, werden Weiterleitungen nicht automatisch behandelt.

Um eine Weiterleitung auszulösen, können Sie die Funktion redirect verwenden, die aus dem Paket @tanstack/react-router exportiert wird

tsx
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page
  throw redirect({
    to: '/',
  })
})
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page
  throw redirect({
    to: '/',
  })
})

Weiterleitungen können alle gleichen Optionen wie router.navigate, useNavigate() und <Link>-Komponenten nutzen. Fühlen Sie sich also frei, auch Folgendes zu übergeben

  • Pfadparameter (Path Params)
  • Suchparameter (Search Params)
  • Hash

Weiterleitungen können auch den Statuscode der Antwort festlegen, indem sie eine status-Option übergeben

tsx
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page with a 301 status code
  throw redirect({
    to: '/',
    status: 301,
  })
})
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page with a 301 status code
  throw redirect({
    to: '/',
    status: 301,
  })
})

Sie können auch zu einem externen Ziel weiterleiten, indem Sie href verwenden

tsx
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const auth = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the auth provider
  throw redirect({
    href: 'https://authprovider.com/login',
  })
})
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const auth = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the auth provider
  throw redirect({
    href: 'https://authprovider.com/login',
  })
})

⚠️ Verwenden Sie nicht die Funktion sendRedirect von @tanstack/react-start/server, um Soft-Redirects aus Server Functions zu senden. Dies sendet die Weiterleitung über den Location-Header und erzwingt eine vollständige Seiten-Hard-Navigation auf dem Client.

Redirect-Header

Sie können auch benutzerdefinierte Header bei einer Weiterleitung festlegen, indem Sie eine headers-Option übergeben

tsx
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page with a custom header
  throw redirect({
    to: '/',
    headers: {
      'X-Custom-Header': 'value',
    },
  })
})
import { redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Redirect the user to the home page with a custom header
  throw redirect({
    to: '/',
    headers: {
      'X-Custom-Header': 'value',
    },
  })
})

Nicht gefunden

Beim Aufruf einer Server Function von einem loader oder beforeLoad-Route-Lifecycle kann ein spezieller notFound-Fehler ausgelöst werden, um dem Router anzuzeigen, dass die angeforderte Ressource nicht gefunden wurde. Dies ist nützlicher als ein einfacher 404-Statuscode, da es Ihnen ermöglicht, eine benutzerdefinierte 404-Seite zu rendern oder den Fehler auf benutzerdefinierte Weise zu behandeln. Wenn `notFound` aus einer Server Function außerhalb eines Route-Lifecycles ausgelöst wird, wird er nicht automatisch behandelt.

Um `notFound` auszulösen, können Sie die Funktion notFound verwenden, die aus dem Paket @tanstack/react-router exportiert wird

tsx
import { notFound } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const getStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Randomly return a not found error
  if (Math.random() < 0.5) {
    throw notFound()
  }

  // Or return some stuff
  return {
    stuff: 'stuff',
  }
})

export const Route = createFileRoute('/stuff')({
  loader: async () => {
    const stuff = await getStuff()

    return {
      stuff,
    }
  },
})
import { notFound } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const getStuff = createServerFn({ method: 'GET' }).handler(async () => {
  // Randomly return a not found error
  if (Math.random() < 0.5) {
    throw notFound()
  }

  // Or return some stuff
  return {
    stuff: 'stuff',
  }
})

export const Route = createFileRoute('/stuff')({
  loader: async () => {
    const stuff = await getStuff()

    return {
      stuff,
    }
  },
})

Not Found-Fehler sind ein Kernfeature von TanStack Router,

Fehlerbehandlung

Wenn eine Server Function einen (Nicht-Weiterleitungs-/Nicht-NotFound-)Fehler auslöst, wird dieser serialisiert und als JSON-Antwort mit einem 500er Statuscode an den Client gesendet. Dies ist nützlich für die Fehlersuche, aber Sie möchten diese Fehler möglicherweise benutzerfreundlicher behandeln. Sie können dies tun, indem Sie den Fehler abfangen und ihn in Ihrem Route-Lifecycle, Ihrer Komponente oder Ihrem Hook behandeln, wie Sie es normalerweise tun würden.

tsx
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  undefined.foo()
})

export const Route = createFileRoute('/stuff')({
  loader: async () => {
    try {
      await doStuff()
    } catch (error) {
      // Handle the error:
      // error === {
      //   message: "Cannot read property 'foo' of undefined",
      //   stack: "TypeError: Cannot read property 'foo' of undefined\n    at doStuff (file:///path/to/file.ts:3:3)"
    }
  },
})
import { createServerFn } from '@tanstack/react-start'

export const doStuff = createServerFn({ method: 'GET' }).handler(async () => {
  undefined.foo()
})

export const Route = createFileRoute('/stuff')({
  loader: async () => {
    try {
      await doStuff()
    } catch (error) {
      // Handle the error:
      // error === {
      //   message: "Cannot read property 'foo' of undefined",
      //   stack: "TypeError: Cannot read property 'foo' of undefined\n    at doStuff (file:///path/to/file.ts:3:3)"
    }
  },
})

No-JS Server Functions

Ohne aktiviertes JavaScript gibt es nur eine Möglichkeit, Server Functions auszuführen: durch das Absenden eines Formulars.

Dies geschieht durch Hinzufügen eines form-Elements zur Seite mit dem HTML-Attribut action.

Beachten Sie, dass wir das HTML-Attribut action erwähnt haben. Dieses Attribut akzeptiert in HTML nur einen String, genau wie alle anderen Attribute.

Während React 19 Unterstützung für die Übergabe einer Funktion an action hinzugefügt hat, ist dies eine React-spezifische Funktion und kein Teil des HTML-Standards.

Das Attribut action teilt dem Browser mit, wohin die Formulardaten gesendet werden sollen, wenn das Formular abgesendet wird. In diesem Fall möchten wir die Formulardaten an die Server Function senden.

Um dies zu tun, können wir die url-Eigenschaft der Server Function nutzen

ts
const yourFn = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const name = formData.get('name')

    if (!name) {
      throw new Error('Name is required')
    }

    return name
  })
  .handler(async ({ data: name }) => {
    console.log(name) // 'John'
  })

console.info(yourFn.url)
const yourFn = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const name = formData.get('name')

    if (!name) {
      throw new Error('Name is required')
    }

    return name
  })
  .handler(async ({ data: name }) => {
    console.log(name) // 'John'
  })

console.info(yourFn.url)

Und dies an das Attribut action des Formulars übergeben

tsx
function Component() {
  return (
    <form action={yourFn.url} method="POST">
      <input name="name" defaultValue="John" />
      <button type="submit">Click me!</button>
    </form>
  )
}
function Component() {
  return (
    <form action={yourFn.url} method="POST">
      <input name="name" defaultValue="John" />
      <button type="submit">Click me!</button>
    </form>
  )
}

Wenn das Formular abgesendet wird, wird die Server Function ausgeführt.

No-JS Server Function Argumente

Um Argumente an eine Server Function zu übergeben, wenn ein Formular abgesendet wird, können Sie das input-Element mit dem name-Attribut verwenden, um das Argument an das FormData anzuhängen, das an Ihre Server Function übergeben wird

tsx
const yourFn = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const age = formData.get('age')

    if (!age) {
      throw new Error('age is required')
    }

    return age.toString()
  })
  .handler(async ({ data: formData }) => {
    // `age` will be '123'
    const age = formData.get('age')
    // ...
  })

function Component() {
  return (
    //  We need to tell the server that our data type is `multipart/form-data` by setting the `encType` attribute on the form.
    <form action={yourFn.url} method="POST" encType="multipart/form-data">
      <input name="age" defaultValue="34" />
      <button type="submit">Click me!</button>
    </form>
  )
}
const yourFn = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const age = formData.get('age')

    if (!age) {
      throw new Error('age is required')
    }

    return age.toString()
  })
  .handler(async ({ data: formData }) => {
    // `age` will be '123'
    const age = formData.get('age')
    // ...
  })

function Component() {
  return (
    //  We need to tell the server that our data type is `multipart/form-data` by setting the `encType` attribute on the form.
    <form action={yourFn.url} method="POST" encType="multipart/form-data">
      <input name="age" defaultValue="34" />
      <button type="submit">Click me!</button>
    </form>
  )
}

Wenn das Formular abgesendet wird, wird die Server Function mit den Daten des Formulars als Argument ausgeführt.

No-JS Server Function Rückgabewert

Unabhängig davon, ob JavaScript aktiviert ist, gibt die Server Function eine Antwort auf die vom Client getätigte HTTP-Anfrage zurück.

Wenn JavaScript aktiviert ist, kann diese Antwort als Rückgabewert der Server Function im JavaScript-Code des Clients abgerufen werden.

ts
const yourFn = createServerFn().handler(async () => {
  return 'Hello, world!'
})

// `.then` is not available when JavaScript is disabled
yourFn().then(console.log)
const yourFn = createServerFn().handler(async () => {
  return 'Hello, world!'
})

// `.then` is not available when JavaScript is disabled
yourFn().then(console.log)

Wenn JavaScript jedoch deaktiviert ist, gibt es keine Möglichkeit, den Rückgabewert der Server Function im JavaScript-Code des Clients abzurufen.

Stattdessen kann die Server Function eine Antwort an den Client geben, die den Browser anweist, auf eine bestimmte Weise zu navigieren.

In Kombination mit einem loader von TanStack Router können wir eine Erfahrung ähnlich einer Single-Page-Anwendung bereitstellen, selbst wenn JavaScript deaktiviert ist; alles, indem wir den Browser anweisen, die aktuelle Seite mit neuen Daten, die durch den loader geleitet werden, neu zu laden

tsx
import * as fs from 'fs'
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const filePath = 'count.txt'

async function readCount() {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn({
  method: 'GET',
}).handler(() => {
  return readCount()
})

const updateCount = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const addBy = formData.get('addBy')

    if (!addBy) {
      throw new Error('addBy is required')
    }

    return parseInt(addBy.toString())
  })
  .handler(async ({ data: addByAmount }) => {
    const count = await readCount()
    await fs.promises.writeFile(filePath, `${count + addByAmount}`)
    // Reload the page to trigger the loader again
    return new Response('ok', { status: 301, headers: { Location: '/' } })
  })

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const state = Route.useLoaderData()

  return (
    <div>
      <form
        action={updateCount.url}
        method="POST"
        encType="multipart/form-data"
      >
        <input type="number" name="addBy" defaultValue="1" />
        <button type="submit">Add</button>
      </form>
      <pre>{state}</pre>
    </div>
  )
}
import * as fs from 'fs'
import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const filePath = 'count.txt'

async function readCount() {
  return parseInt(
    await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'),
  )
}

const getCount = createServerFn({
  method: 'GET',
}).handler(() => {
  return readCount()
})

const updateCount = createServerFn({ method: 'POST' })
  .validator((formData) => {
    if (!(formData instanceof FormData)) {
      throw new Error('Invalid form data')
    }

    const addBy = formData.get('addBy')

    if (!addBy) {
      throw new Error('addBy is required')
    }

    return parseInt(addBy.toString())
  })
  .handler(async ({ data: addByAmount }) => {
    const count = await readCount()
    await fs.promises.writeFile(filePath, `${count + addByAmount}`)
    // Reload the page to trigger the loader again
    return new Response('ok', { status: 301, headers: { Location: '/' } })
  })

export const Route = createFileRoute('/')({
  component: Home,
  loader: async () => await getCount(),
})

function Home() {
  const state = Route.useLoaderData()

  return (
    <div>
      <form
        action={updateCount.url}
        method="POST"
        encType="multipart/form-data"
      >
        <input type="number" name="addBy" defaultValue="1" />
        <button type="submit">Add</button>
      </form>
      <pre>{state}</pre>
    </div>
  )
}

Statische Server-Funktionen

Bei der Verwendung von Prerendering/Static Generation können Server Functions auch "statisch" sein, was es ermöglicht, ihre Ergebnisse zur Build-Zeit zu cachen und als statische Assets auszuliefern.

Erfahren Sie alles über dieses Muster auf der Seite Static Server Functions.

Wie werden Server Functions kompiliert?

Unter der Haube werden Server Functions aus dem Client-Bundle in ein separates Server-Bundle extrahiert. Auf dem Server werden sie unverändert ausgeführt und das Ergebnis an den Client zurückgesendet. Auf dem Client leiten Server Functions die Anfrage an den Server weiter, der die Funktion ausführt und das Ergebnis über fetch an den Client zurücksendet.

Der Prozess sieht wie folgt aus

  • Wenn createServerFn in einer Datei gefunden wird, wird die innere Funktion auf eine use server-Direktive geprüft
  • Wenn die use server-Direktive fehlt, wird sie am Anfang der Funktion hinzugefügt
  • Auf dem Client wird die innere Funktion aus dem Client-Bundle in ein separates Server-Bundle extrahiert
  • Die clientseitige Server Function wird durch eine Proxy-Funktion ersetzt, die eine Anfrage an den Server sendet, um die extrahierte Funktion auszuführen
  • Auf dem Server wird die Server Function nicht extrahiert und unverändert ausgeführt
  • Nach der Extraktion wendet jedes Bundle einen Prozess der Dead-Code-Eliminierung an, um ungenutzten Code aus jedem Bundle zu entfernen.
Unsere Partner
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Prisma
Bytes abonnieren

Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.

Bytes

Kein Spam. Jederzeit kündbar.

Bytes abonnieren

Ihre wöchentliche Dosis JavaScript-Nachrichten. Jeden Montag kostenlos an über 100.000 Entwickler geliefert.

Bytes

Kein Spam. Jederzeit kündbar.