Created
October 6, 2025 23:47
-
-
Save vasco3/f499f7c5a453b04e1be75bb6b6964f76 to your computer and use it in GitHub Desktop.
convex_tanstack_guidelines.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Convex + TanStack Start Integration Guidelines | |
Esta sección describe cómo integrar Convex eficientemente con TanStack Start, cubriendo los patrones más comunes para queries, mutations y actions. | |
## Setup inicial | |
Antes de usar estos patrones, asegúrate de tener configurado el ConvexProvider en tu aplicación TanStack Start: | |
```ts | |
// src/root.tsx o src/router.tsx | |
import { ConvexProvider, ConvexReactClient } from "convex/react"; | |
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL!); | |
export function App() { | |
return <ConvexProvider client={convex}>{/* Tu aplicación */}</ConvexProvider>; | |
} | |
``` | |
## Patrón 1: convexQuery + useQuery | |
Este patrón combina los hooks de Convex con TanStack Query directamente en los componentes para obtener control avanzado sobre el caché y el estado de las queries. | |
- Note: useQuery can be "disabled" by passing in "skip" instead of its arguments. | |
### Ejemplo básico | |
```ts | |
// components/MessageList.tsx | |
import { convexQuery } from "@convex-dev/react-query"; | |
import { useQuery } from "@tanstack/react-query"; | |
import { api } from "convex/_generated/api"; | |
import { Id } from "convex/_generated/dataModel"; | |
interface MessageListProps { | |
channelId: Id<"channels">; | |
} | |
export function MessageList({ channelId }: MessageListProps) { | |
const { | |
data: messages, | |
isLoading, | |
error, | |
} = useQuery(convexQuery(api.messages.listMessages, { channelId })); | |
if (isLoading) return <div>Cargando mensajes...</div>; | |
if (error) return <div>Error al cargar mensajes: {error.message}</div>; | |
return ( | |
<div> | |
{messages?.map((message) => ( | |
<div key={message._id} className="p-2 border rounded"> | |
{message.content} | |
</div> | |
))} | |
</div> | |
); | |
} | |
``` | |
### Ejemplo con skip condicional | |
> Loading a query conditionally | |
- useQuery can be "disabled" by passing in "skip" instead of its arguments: | |
```ts | |
// Ejemplo simple de skip basado en parámetros de URL | |
import { useQuery } from "convex/react"; | |
import { api } from "../convex/_generated/api"; | |
export function App() { | |
const param = new URLSearchParams(window.location.search).get("param"); | |
const data = useQuery( | |
api.functions.read, | |
param !== null ? { param } : "skip" | |
); | |
//... | |
} | |
``` | |
### Ejemplo avanzado con opciones de TanStack Query | |
```ts | |
// components/MessageListAdvanced.tsx | |
import { convexQuery } from "@convex-dev/react-query"; | |
import { useQuery } from "@tanstack/react-query"; | |
import { api } from "convex/_generated/api"; | |
import { Id } from "convex/_generated/dataModel"; | |
interface MessageListAdvancedProps { | |
channelId: Id<"channels">; | |
} | |
export function MessageListAdvanced({ channelId }: MessageListAdvancedProps) { | |
const { data: messages } = useQuery( | |
convexQuery(api.messages.listMessages, { channelId }) | |
); | |
return ( | |
<div> | |
<div> | |
<h3>Mensajes ({messages?.length || 0})</h3> | |
<button onClick={() => refetch()}>Actualizar</button> | |
</div> | |
{messages?.map((message) => ( | |
<div key={message._id}>{message.content}</div> | |
))} | |
</div> | |
); | |
} | |
``` | |
## Patrón 2: useConvexMutation + useMutation | |
Este patrón permite usar mutations de Convex directamente en componentes con las capacidades avanzadas de TanStack Query como optimistic updates y invalidación inteligente de caché. | |
### Ejemplo básico | |
```ts | |
// components/MessageForm.tsx | |
import { useState } from "react"; | |
import { useConvexMutation } from "@convex-dev/react-query"; | |
import { useMutation, useQueryClient } from "@tanstack/react-query"; | |
import { api } from "convex/_generated/api"; | |
import { Id } from "convex/_generated/dataModel"; | |
interface MessageFormProps { | |
channelId: Id<"channels">; | |
authorId: Id<"users">; | |
} | |
export function MessageForm({ channelId, authorId }: MessageFormProps) { | |
const [content, setContent] = useState(""); | |
const queryClient = useQueryClient(); | |
const createMessage = useMutation({ | |
mutationFn: useConvexMutation(api.messages.sendMessage), | |
}); | |
const handleSubmit = async (e: React.FormEvent) => { | |
e.preventDefault(); | |
if (!content.trim()) return; | |
try { | |
await createMessage.mutateAsync({ | |
channelId, | |
authorId, | |
content: content.trim(), | |
}); | |
} catch (error) { | |
// Error ya manejado por la configuración del useMutation | |
console.error("Error enviando mensaje:", error); | |
} | |
}; | |
return ( | |
<form onSubmit={handleSubmit}> | |
<textarea | |
value={content} | |
onChange={(e) => setContent(e.target.value)} | |
placeholder="Escribe tu mensaje..." | |
disabled={createMessage.isPending} | |
/> | |
<button | |
type="submit" | |
disabled={createMessage.isPending || !content.trim()} | |
> | |
{createMessage.isPending ? "Enviando..." : "Enviar"} | |
</button> | |
{createMessage.error && <div>Error: {createMessage.error.message}</div>} | |
</form> | |
); | |
} | |
``` | |
## Patrón 3: useConvexAction + useMutation | |
Este patrón es ideal para actions de Convex que realizan operaciones complejas o requieren integración con servicios externos, implementado directamente en los componentes. | |
### Ejemplo básico | |
```ts | |
// components/AIResponseGenerator.tsx | |
import { useConvexAction } from "@convex-dev/react-query"; | |
import { useMutation, useQueryClient } from "@tanstack/react-query"; | |
import { api } from "convex/_generated/api"; | |
import { Id } from "convex/_generated/dataModel"; | |
interface AIResponseGeneratorProps { | |
channelId: Id<"channels">; | |
} | |
export function AIResponseGenerator({ channelId }: AIResponseGeneratorProps) { | |
const queryClient = useQueryClient(); | |
const generateResponse = useMutation({ | |
mutationFn: useConvexAction(api.messages.generateResponse), | |
}); | |
const handleGenerate = async () => { | |
try { | |
await generateResponse.mutateAsync({ channelId }); | |
} catch (error) { | |
// Error ya manejado por la configuración del useMutation | |
console.error("Error generando respuesta:", error); | |
} | |
}; | |
return ( | |
<div> | |
<button onClick={handleGenerate} disabled={generateResponse.isPending}> | |
{generateResponse.isPending | |
? "Generando respuesta..." | |
: "Generar respuesta IA"} | |
</button> | |
</div> | |
); | |
} | |
``` | |
### Ejemplo avanzado con polling y progreso | |
```ts | |
// components/DocumentProcessor.tsx | |
import { useConvexAction, convexQuery } from "@convex-dev/react-query"; | |
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; | |
import { api } from "convex/_generated/api"; | |
import { Id } from "convex/_generated/dataModel"; | |
interface DocumentProcessorProps { | |
documentId: Id<"documents">; | |
} | |
export function DocumentProcessor({ documentId }: DocumentProcessorProps) { | |
const queryClient = useQueryClient(); | |
// Action para procesar documento | |
const processDocument = useMutation({ | |
mutationFn: useConvexAction(api.documents.processDocument), | |
}); | |
// Query para monitorear progreso con polling | |
const { data: progress } = useQuery( | |
convexQuery( | |
api.documents.getProgress, | |
!!documentId ? { documentId } : "skip" | |
) | |
); | |
const handleProcess = async () => { | |
try { | |
await processDocument.mutateAsync({ documentId }); | |
// Puedes ejecutar código adicional después del éxito aquí | |
} catch (error) { | |
// Los errores se manejan en la configuración del mutation | |
console.error("Error procesando documento:", error); | |
} | |
}; | |
return ( | |
<div> | |
<div> | |
<button | |
onClick={handleProcess} | |
disabled={ | |
processDocument.isPending || progress?.status === "processing" | |
} | |
> | |
{progress?.status === "processing" | |
? "Procesando..." | |
: "Procesar Documento"} | |
</button> | |
{progress?.status && <span>Estado: {progress.status}</span>} | |
</div> | |
{progress?.status === "processing" && ( | |
<div> | |
<div> | |
<span>Progreso</span> | |
<span>{progress.progress || 0}%</span> | |
</div> | |
<div> | |
<div style={{ width: `${progress.progress || 0}%` }} /> | |
</div> | |
</div> | |
)} | |
{progress?.status === "error" && ( | |
<div> | |
<div>Error procesando documento</div> | |
<div>{progress.error}</div> | |
<button onClick={handleProcess}>Reintentar</button> | |
</div> | |
)} | |
{progress?.status === "completed" && ( | |
<div> | |
<div>¡Documento procesado exitosamente!</div> | |
{progress.result && ( | |
<div>Resultado: {JSON.stringify(progress.result)}</div> | |
)} | |
</div> | |
)} | |
{processDocument.error && ( | |
<div>Error del sistema: {processDocument.error.message}</div> | |
)} | |
</div> | |
); | |
} | |
``` | |
## Mejores prácticas | |
### 1. Manejo centralizado de errores | |
Usa sonner toast para mostrar notificaciones |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment