React 19 Server Actions et useActionState : Le guide complet de la gestion des formulaires

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Des formulaires sans boilerplate. React 19 change fondamentalement la gestion des formulaires — les Server Actions et useActionState vous permettent de construire des formulaires type-safe qui fonctionnent même avant le chargement du JavaScript.

Ce que vous allez apprendre

  • Comment les Server Actions remplacent les routes API pour les mutations de formulaires
  • Utiliser useActionState pour la gestion de l'état des formulaires avec les états de chargement
  • Construire des formulaires progressivement améliorés qui fonctionnent sans JavaScript
  • La validation type-safe côté serveur avec Zod
  • Les mises à jour optimistes avec useOptimistic
  • Des patterns concrets : formulaires multi-étapes, upload de fichiers et gestion des erreurs

Prérequis

Avant de commencer ce tutoriel, assurez-vous d'avoir :

  • Node.js 20+ installé
  • Des connaissances de base en React et TypeScript
  • Une familiarité avec le Next.js App Router (pages, layouts, composants serveur)
  • Un éditeur de code comme VS Code

Pourquoi les Server Actions changent tout

Avant React 19, la gestion des formulaires en React nécessitait généralement :

  1. Créer une route API séparée (/api/submit-form)
  2. Utiliser useState pour chaque champ du formulaire
  3. Écrire des handlers onChange pour chaque input
  4. Gérer manuellement les états de chargement, erreur et succès
  5. Appeler fetch() ou une bibliothèque pour soumettre les données

C'est beaucoup de plomberie pour quelque chose d'aussi fondamental qu'un formulaire. Les Server Actions éliminent la majeure partie de cette complexité en vous permettant de définir des fonctions serveur que React appelle directement depuis le client — sans routes API, sans appels fetch manuels, sans gestion d'état séparée.

// Avant : L'ancienne méthode
'use client'
import { useState } from 'react'
 
export default function ContactForm() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')
 
  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setLoading(true)
    try {
      const res = await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify({ name, email }),
      })
      if (!res.ok) throw new Error('Failed')
    } catch (err) {
      setError('Something went wrong')
    } finally {
      setLoading(false)
    }
  }
 
  return (
    <form onSubmit={handleSubmit}>
      {/* ... beaucoup d'inputs contrôlés */}
    </form>
  )
}
// Après : Avec les Server Actions
import { submitContact } from './actions'
 
export default function ContactForm() {
  return (
    <form action={submitContact}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit">Envoyer</button>
    </form>
  )
}

La version "après" fonctionne sans JavaScript, n'a pas d'état côté client, et la fonction serveur gère tout.

Étape 1 : Configuration du projet

Créons un nouveau projet Next.js 15 avec React 19 :

npx create-next-app@latest react19-forms --typescript --tailwind --app --src-dir
cd react19-forms

Vérifiez que React 19 est dans votre package.json :

{
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "next": "^15.0.0"
  }
}

Installez Zod pour la validation côté serveur :

npm install zod

Étape 2 : Votre première Server Action

Créez un fichier pour vos actions serveur. La directive "use server" en haut du fichier indique à React que ce module s'exécute uniquement sur le serveur :

// src/app/actions.ts
'use server'
 
export async function createUser(formData: FormData) {
  const name = formData.get('name') as string
  const email = formData.get('email') as string
 
  // Ceci s'exécute sur le serveur — accès sécurisé aux bases de données, secrets, etc.
  console.log('Creating user:', { name, email })
 
  // Simulation d'insertion en base de données
  await new Promise((resolve) => setTimeout(resolve, 1000))
 
  return { success: true, message: `User ${name} created!` }
}

Maintenant utilisez-la dans un formulaire. C'est un Composant Serveur — pas besoin de "use client" :

// src/app/page.tsx
import { createUser } from './actions'
 
export default function HomePage() {
  return (
    <main className="max-w-md mx-auto mt-20 p-6">
      <h1 className="text-2xl font-bold mb-6">Créer un compte</h1>
      <form action={createUser} className="space-y-4">
        <div>
          <label htmlFor="name" className="block text-sm font-medium mb-1">
            Nom
          </label>
          <input
            id="name"
            name="name"
            type="text"
            required
            className="w-full border rounded-lg px-3 py-2"
          />
        </div>
        <div>
          <label htmlFor="email" className="block text-sm font-medium mb-1">
            Email
          </label>
          <input
            id="email"
            name="email"
            type="email"
            required
            className="w-full border rounded-lg px-3 py-2"
          />
        </div>
        <button
          type="submit"
          className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700"
        >
          Créer
        </button>
      </form>
    </main>
  )
}

Ce formulaire fonctionne même avec JavaScript désactivé ! Le navigateur soumet le formulaire nativement, et Next.js gère la Server Action côté serveur.

Étape 3 : Ajouter useActionState pour un retour enrichi

Le formulaire basique fonctionne, mais il n'y a pas de retour — pas d'état de chargement, pas de messages d'erreur, pas de confirmation de succès. C'est là qu'intervient useActionState.

useActionState est un hook React 19 qui gère le cycle de vie d'une action de formulaire :

const [state, formAction, isPending] = useActionState(action, initialState)
  • state — La valeur de retour actuelle de l'action (erreurs, messages de succès, etc.)
  • formAction — Une version encapsulée de votre action à passer à <form action={...}>
  • isPending — Booléen indiquant si l'action est en cours d'exécution

D'abord, mettez à jour votre Server Action pour retourner un état structuré :

// src/app/actions.ts
'use server'
 
export type FormState = {
  success: boolean
  message: string
  errors?: {
    name?: string[]
    email?: string[]
  }
}
 
export async function createUser(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  const name = formData.get('name') as string
  const email = formData.get('email') as string
 
  // Validation basique
  const errors: FormState['errors'] = {}
  if (!name || name.length < 2) {
    errors.name = ['Le nom doit contenir au moins 2 caractères']
  }
  if (!email || !email.includes('@')) {
    errors.email = ['Veuillez entrer un email valide']
  }
 
  if (Object.keys(errors).length > 0) {
    return { success: false, message: 'La validation a échoué', errors }
  }
 
  // Simulation d'opération en base de données
  await new Promise((resolve) => setTimeout(resolve, 1000))
 
  return { success: true, message: `Bienvenue, ${name} !` }
}

Notez que la signature de la fonction a changé — useActionState passe l'état précédent comme premier argument et formData comme second. C'est différent d'une Server Action simple où formData est le seul argument.

Maintenant créez le composant client avec useActionState :

// src/app/create-user-form.tsx
'use client'
 
import { useActionState } from 'react'
import { createUser, type FormState } from './actions'
 
const initialState: FormState = {
  success: false,
  message: '',
}
 
export default function CreateUserForm() {
  const [state, formAction, isPending] = useActionState(createUser, initialState)
 
  return (
    <form action={formAction} className="space-y-4">
      {/* Message de succès */}
      {state.success && (
        <div className="bg-green-50 border border-green-200 text-green-800 px-4 py-3 rounded-lg">
          {state.message}
        </div>
      )}
 
      {/* Message d'erreur général */}
      {!state.success && state.message && (
        <div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded-lg">
          {state.message}
        </div>
      )}
 
      <div>
        <label htmlFor="name" className="block text-sm font-medium mb-1">
          Nom
        </label>
        <input
          id="name"
          name="name"
          type="text"
          required
          className="w-full border rounded-lg px-3 py-2"
          aria-describedby="name-error"
        />
        {state.errors?.name && (
          <p id="name-error" className="text-red-600 text-sm mt-1">
            {state.errors.name[0]}
          </p>
        )}
      </div>
 
      <div>
        <label htmlFor="email" className="block text-sm font-medium mb-1">
          Email
        </label>
        <input
          id="email"
          name="email"
          type="email"
          required
          className="w-full border rounded-lg px-3 py-2"
          aria-describedby="email-error"
        />
        {state.errors?.email && (
          <p id="email-error" className="text-red-600 text-sm mt-1">
            {state.errors.email[0]}
          </p>
        )}
      </div>
 
      <button
        type="submit"
        disabled={isPending}
        className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        {isPending ? 'Création en cours...' : 'Créer le compte'}
      </button>
    </form>
  )
}

Mettez à jour votre page pour utiliser le nouveau composant :

// src/app/page.tsx
import CreateUserForm from './create-user-form'
 
export default function HomePage() {
  return (
    <main className="max-w-md mx-auto mt-20 p-6">
      <h1 className="text-2xl font-bold mb-6">Créer un compte</h1>
      <CreateUserForm />
    </main>
  )
}

Étape 4 : Validation type-safe avec Zod

La validation codée en dur est fragile. Utilisons Zod pour une validation robuste et type-safe :

// src/lib/schemas.ts
import { z } from 'zod'
 
export const createUserSchema = z.object({
  name: z
    .string()
    .min(2, 'Le nom doit contenir au moins 2 caractères')
    .max(50, 'Le nom doit contenir moins de 50 caractères'),
  email: z
    .string()
    .email('Veuillez entrer une adresse email valide'),
  password: z
    .string()
    .min(8, 'Le mot de passe doit contenir au moins 8 caractères')
    .regex(/[A-Z]/, 'Le mot de passe doit contenir au moins une majuscule')
    .regex(/[0-9]/, 'Le mot de passe doit contenir au moins un chiffre'),
})
 
export type CreateUserInput = z.infer<typeof createUserSchema>

Mettez à jour la Server Action pour utiliser Zod :

// src/app/actions.ts
'use server'
 
import { createUserSchema } from '@/lib/schemas'
 
export type FormState = {
  success: boolean
  message: string
  errors?: Record<string, string[]>
}
 
export async function createUser(
  prevState: FormState,
  formData: FormData
): Promise<FormState> {
  // Parsing et validation avec Zod
  const result = createUserSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  })
 
  if (!result.success) {
    return {
      success: false,
      message: 'Veuillez corriger les erreurs ci-dessous',
      errors: result.error.flatten().fieldErrors as Record<string, string[]>,
    }
  }
 
  // result.data est maintenant entièrement typé comme CreateUserInput
  const { name, email, password } = result.data
 
  try {
    // Simulation d'insertion en base de données
    await new Promise((resolve) => setTimeout(resolve, 1000))
 
    // En production : hasher le mot de passe, insérer en base
    console.log('Creating user:', { name, email })
 
    return { success: true, message: `Compte créé pour ${name} !` }
  } catch (error) {
    return { success: false, message: 'Échec de la création du compte. Veuillez réessayer.' }
  }
}

Validez toujours côté serveur, même si vous validez aussi côté client. La validation côté client peut être contournée — la validation côté serveur est votre frontière de sécurité.

Étape 5 : Mises à jour optimistes avec useOptimistic

Pour les actions où vous souhaitez un retour instantané (comme ajouter un favori ou poster un commentaire), React 19 fournit useOptimistic :

// src/app/comments/comment-form.tsx
'use client'
 
import { useActionState, useOptimistic } from 'react'
import { addComment, type CommentState } from './actions'
 
type Comment = {
  id: string
  text: string
  author: string
  pending?: boolean
}
 
export default function CommentSection({
  initialComments,
}: {
  initialComments: Comment[]
}) {
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    initialComments,
    (state: Comment[], newComment: Comment) => [
      ...state,
      { ...newComment, pending: true },
    ]
  )
 
  const initialState: CommentState = { success: false, message: '' }
  const [state, formAction, isPending] = useActionState(
    async (prevState: CommentState, formData: FormData) => {
      // Ajouter le commentaire optimiste immédiatement
      addOptimisticComment({
        id: crypto.randomUUID(),
        text: formData.get('text') as string,
        author: 'Vous',
        pending: true,
      })
 
      // Puis exécuter la server action réelle
      return addComment(prevState, formData)
    },
    initialState
  )
 
  return (
    <div className="space-y-4">
      {/* Liste des commentaires */}
      <ul className="space-y-3">
        {optimisticComments.map((comment) => (
          <li
            key={comment.id}
            className={`p-3 rounded-lg border ${
              comment.pending ? 'opacity-50 bg-gray-50' : 'bg-white'
            }`}
          >
            <p className="font-medium">{comment.author}</p>
            <p className="text-gray-600">{comment.text}</p>
            {comment.pending && (
              <span className="text-xs text-gray-400">Envoi en cours...</span>
            )}
          </li>
        ))}
      </ul>
 
      {/* Formulaire de commentaire */}
      <form action={formAction} className="flex gap-2">
        <input
          name="text"
          placeholder="Ajouter un commentaire..."
          required
          className="flex-1 border rounded-lg px-3 py-2"
        />
        <button
          type="submit"
          disabled={isPending}
          className="bg-blue-600 text-white px-4 py-2 rounded-lg disabled:opacity-50"
        >
          Publier
        </button>
      </form>
    </div>
  )
}

Le commentaire apparaît instantanément dans l'interface avec un état visuel "en attente", puis est confirmé (ou annulé) lorsque le serveur répond.

Étape 6 : Pattern de formulaire multi-étapes

Pour les formulaires complexes comme les flux d'onboarding, vous pouvez combiner les Server Actions avec l'état client pour construire des assistants multi-étapes :

// src/app/onboarding/onboarding-form.tsx
'use client'
 
import { useState } from 'react'
import { useActionState } from 'react'
import { completeOnboarding, type OnboardingState } from './actions'
 
const steps = ['Profil', 'Préférences', 'Vérification'] as const
 
export default function OnboardingForm() {
  const [currentStep, setCurrentStep] = useState(0)
 
  const initialState: OnboardingState = {
    success: false,
    message: '',
    step: 0,
  }
 
  const [state, formAction, isPending] = useActionState(
    completeOnboarding,
    initialState
  )
 
  return (
    <div className="max-w-lg mx-auto">
      {/* Barre de progression */}
      <div className="flex mb-8">
        {steps.map((step, index) => (
          <div
            key={step}
            className={`flex-1 text-center py-2 text-sm font-medium ${
              index <= currentStep
                ? 'text-blue-600 border-b-2 border-blue-600'
                : 'text-gray-400 border-b-2 border-gray-200'
            }`}
          >
            {step}
          </div>
        ))}
      </div>
 
      <form action={formAction}>
        {/* Champ caché pour suivre l'étape */}
        <input type="hidden" name="step" value={currentStep} />
 
        {/* Étape 1 : Profil */}
        {currentStep === 0 && (
          <div className="space-y-4">
            <input name="fullName" placeholder="Nom complet" required
              className="w-full border rounded-lg px-3 py-2" />
            <input name="company" placeholder="Entreprise" required
              className="w-full border rounded-lg px-3 py-2" />
          </div>
        )}
 
        {/* Étape 2 : Préférences */}
        {currentStep === 1 && (
          <div className="space-y-4">
            <select name="role" className="w-full border rounded-lg px-3 py-2">
              <option value="developer">Développeur</option>
              <option value="designer">Designer</option>
              <option value="manager">Chef de produit</option>
            </select>
            <select name="experience" className="w-full border rounded-lg px-3 py-2">
              <option value="junior">Junior (0-2 ans)</option>
              <option value="mid">Intermédiaire (2-5 ans)</option>
              <option value="senior">Senior (5+ ans)</option>
            </select>
          </div>
        )}
 
        {/* Étape 3 : Vérification */}
        {currentStep === 2 && (
          <div className="bg-gray-50 p-4 rounded-lg">
            <p className="text-gray-600">
              Vérifiez vos informations et cliquez sur Soumettre pour terminer la configuration.
            </p>
          </div>
        )}
 
        {/* Navigation */}
        <div className="flex justify-between mt-6">
          <button
            type="button"
            onClick={() => setCurrentStep((s) => Math.max(0, s - 1))}
            className={`px-4 py-2 rounded-lg border ${
              currentStep === 0 ? 'invisible' : ''
            }`}
          >
            Précédent
          </button>
 
          {currentStep < steps.length - 1 ? (
            <button
              type="button"
              onClick={() => setCurrentStep((s) => s + 1)}
              className="px-4 py-2 bg-blue-600 text-white rounded-lg"
            >
              Suivant
            </button>
          ) : (
            <button
              type="submit"
              disabled={isPending}
              className="px-4 py-2 bg-green-600 text-white rounded-lg disabled:opacity-50"
            >
              {isPending ? 'Envoi en cours...' : 'Terminer la configuration'}
            </button>
          )}
        </div>
      </form>
    </div>
  )
}

Étape 7 : Upload de fichiers avec les Server Actions

Les Server Actions gèrent nativement les uploads de fichiers via FormData :

// src/app/upload/actions.ts
'use server'
 
import { writeFile } from 'fs/promises'
import path from 'path'
 
export type UploadState = {
  success: boolean
  message: string
  url?: string
}
 
export async function uploadAvatar(
  prevState: UploadState,
  formData: FormData
): Promise<UploadState> {
  const file = formData.get('avatar') as File
 
  if (!file || file.size === 0) {
    return { success: false, message: 'Veuillez sélectionner un fichier' }
  }
 
  // Validation du type de fichier
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
  if (!allowedTypes.includes(file.type)) {
    return { success: false, message: 'Seules les images JPEG, PNG et WebP sont autorisées' }
  }
 
  // Validation de la taille (max 5 Mo)
  const maxSize = 5 * 1024 * 1024
  if (file.size > maxSize) {
    return { success: false, message: 'Le fichier doit faire moins de 5 Mo' }
  }
 
  try {
    const bytes = await file.arrayBuffer()
    const buffer = Buffer.from(bytes)
 
    const filename = `${Date.now()}-${file.name}`
    const uploadPath = path.join(process.cwd(), 'public', 'uploads', filename)
 
    await writeFile(uploadPath, buffer)
 
    return {
      success: true,
      message: 'Avatar uploadé avec succès !',
      url: `/uploads/${filename}`,
    }
  } catch (error) {
    return { success: false, message: "Échec de l'upload. Veuillez réessayer." }
  }
}
// src/app/upload/avatar-form.tsx
'use client'
 
import { useActionState, useRef } from 'react'
import { uploadAvatar, type UploadState } from './actions'
 
const initialState: UploadState = { success: false, message: '' }
 
export default function AvatarUploadForm() {
  const [state, formAction, isPending] = useActionState(uploadAvatar, initialState)
  const formRef = useRef<HTMLFormElement>(null)
 
  return (
    <form ref={formRef} action={formAction} className="space-y-4">
      <div>
        <label className="block text-sm font-medium mb-2">Photo de profil</label>
        <input
          name="avatar"
          type="file"
          accept="image/jpeg,image/png,image/webp"
          required
          className="block w-full text-sm file:mr-4 file:py-2 file:px-4
            file:rounded-lg file:border-0 file:bg-blue-50 file:text-blue-700
            hover:file:bg-blue-100"
        />
      </div>
 
      {state.message && (
        <div className={`px-4 py-3 rounded-lg text-sm ${
          state.success
            ? 'bg-green-50 text-green-800'
            : 'bg-red-50 text-red-800'
        }`}>
          {state.message}
        </div>
      )}
 
      {state.url && (
        <img
          src={state.url}
          alt="Avatar uploadé"
          className="w-24 h-24 rounded-full object-cover"
        />
      )}
 
      <button
        type="submit"
        disabled={isPending}
        className="bg-blue-600 text-white px-4 py-2 rounded-lg disabled:opacity-50"
      >
        {isPending ? 'Upload en cours...' : 'Uploader la photo'}
      </button>
    </form>
  )
}

Étape 8 : Pattern helper réutilisable pour les formulaires

Au fur et à mesure que votre application grandit, vous voudrez un pattern réutilisable. Voici un helper générique :

// src/lib/form-utils.ts
import { z } from 'zod'
 
export type ActionState<T = undefined> = {
  success: boolean
  message: string
  errors?: Record<string, string[]>
  data?: T
}
 
export function createFormAction<TSchema extends z.ZodObject<any>, TResult = void>(
  schema: TSchema,
  handler: (data: z.infer<TSchema>) => Promise<TResult>
) {
  return async (
    prevState: ActionState<TResult>,
    formData: FormData
  ): Promise<ActionState<TResult>> => {
    const raw = Object.fromEntries(formData.entries())
    const result = schema.safeParse(raw)
 
    if (!result.success) {
      return {
        success: false,
        message: 'La validation a échoué',
        errors: result.error.flatten().fieldErrors as Record<string, string[]>,
      }
    }
 
    try {
      const data = await handler(result.data)
      return { success: true, message: 'Succès', data: data as TResult }
    } catch (error) {
      return {
        success: false,
        message: error instanceof Error ? error.message : "Une erreur s'est produite",
      }
    }
  }
}

Créer une nouvelle action de formulaire devient trivial :

// src/app/actions.ts
'use server'
 
import { createFormAction } from '@/lib/form-utils'
import { createUserSchema } from '@/lib/schemas'
 
export const createUser = createFormAction(createUserSchema, async (data) => {
  // data est entièrement typé comme { name: string, email: string, password: string }
  await db.user.create({ data })
  return { id: crypto.randomUUID() }
})

Tester votre implémentation

Lancez le serveur de développement :

npm run dev

Testez les scénarios suivants :

  1. Cas nominal — Remplissez tous les champs correctement et soumettez. Vous devriez voir un message de succès.
  2. Erreurs de validation — Soumettez avec un email invalide ou un mot de passe trop court. Des erreurs au niveau des champs devraient apparaître.
  3. JavaScript désactivé — Désactivez JS dans les DevTools et soumettez le formulaire. Il devrait fonctionner via un rechargement complet de la page.
  4. État de chargement — Cliquez sur soumettre et observez le bouton désactivé avec le texte "Création en cours...".
  5. Erreurs réseau — Simulez une panne serveur et vérifiez que le message d'erreur apparaît.

Dépannage

"Functions cannot be passed directly to Client Components"

Cette erreur signifie que vous essayez de passer une Server Action comme prop à un composant client de manière incorrecte. Assurez-vous que :

  • La Server Action est définie dans un fichier 'use server'
  • Vous l'importez directement dans le composant client, ou la passez via la prop action du formulaire

"useActionState is not a function"

Assurez-vous d'être sur React 19+. Vérifiez votre package.json — si vous voyez React 18.x, mettez à jour :

npm install react@latest react-dom@latest

Les données du formulaire sont vides

Assurez-vous que chaque input a un attribut name. FormData utilise l'attribut name pour collecter les valeurs — sans lui, le champ est invisible pour le serveur.

Points clés

PatternQuand l'utiliser
action={serverAction} simpleFormulaires simples dans les composants serveur, pas besoin de retour
useActionStateFormulaires nécessitant des états de chargement, erreurs et messages de succès
useOptimisticRetour instantané pour les actions (commentaires, likes, toggles)
Helper createFormActionPattern réutilisable pour les actions validées dans toute l'app

Prochaines étapes

  • Explorez revalidatePath et revalidateTag pour rafraîchir les données en cache après les mutations
  • Construisez une application CRUD complète combinant les Server Actions avec Prisma ou Drizzle ORM
  • Ajoutez du rate limiting à vos Server Actions pour la production
  • Combinez avec la bibliothèque next-safe-action pour encore plus de sécurité de typage

Conclusion

Les Server Actions et useActionState de React 19 représentent une simplification majeure dans la construction des formulaires. En déplaçant la logique de validation et de mutation vers le serveur, vous obtenez l'amélioration progressive gratuitement, éliminez des catégories entières de bugs côté client, et écrivez considérablement moins de code. Les patterns de ce tutoriel — des actions basiques aux mises à jour optimistes en passant par les helpers réutilisables — vous donnent une base solide pour construire des formulaires prêts pour la production dans n'importe quelle application Next.js.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur 11 Les Bases de Laravel 11 : Generation d'URL.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·