Comentarios y Discusiones
Reglas
- Comentarios en hilo: clave foránea
parent_id en la tabla de comentarios — null para nivel superior, referencia al padre para respuestas
- IU optimista: agregar comentario a la UI de inmediato, confirmar con respuesta del servidor, revertir en error
- @menciones: analizar
@usuario en el cuerpo del comentario — mostrar menú desplegable de búsqueda de usuario al escribir @, notificar a los usuarios mencionados
- Reacciones: tabla separada
reactions (commentId, userId, emoji) — restricción única en (commentId, userId, emoji)
- Moderación: campo
status (publicado, marcado, eliminado) — cola de moderación para contenido marcado, marcado automático con lista de palabras
- Borrado suave: establecer marca de tiempo
deletedAt — mostrar marcador de posición "eliminado" para comentarios con respuestas, ocultar completamente comentarios eliminados huérfanos
- Paginación por cursor:
cursor (commentId) + limit — ordenado por createdAt, cargar comentarios más antiguos al hacer scroll
- Historial de ediciones: almacenar ediciones en tabla separada o arreglo JSON — mostrar indicador "editado" con la última marca de tiempo de edición
Patrones
const comments = await db.query.comments.findMany({
where: and(eq(comments.postId, postId), isNull(comments.parentId)),
with: {
author: { columns: { id: true, name: true, avatar: true } },
replies: {
with: { author: { columns: { id: true, name: true, avatar: true } } },
orderBy: [asc(comments.createdAt)],
},
reactions: true,
},
orderBy: [desc(comments.createdAt)],
limit: 20,
});
const addComment = useMutation({
mutationFn: (body: string) => api.post(`/posts/${postId}/comments`, { body }),
onMutate: async (body) => {
await queryClient.cancelQueries({ queryKey: ["comments", postId] });
const previous = queryClient.getQueryData(["comments", postId]);
queryClient.setQueryData(["comments", postId], (old: Comment[]) => [
{ id: `temp-${Date.now()}`, body, author: currentUser, createdAt: new Date() },
...old,
]);
return { previous };
},
onError: (_err, _body, context) => {
queryClient.setQueryData(["comments", postId], context?.previous);
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ["comments", postId] }),
});
Evitar
- Profundidad de anidación ilimitada — limitar a 2-3 niveles, luego aplanar respuestas más profundas
- Cargar todos los comentarios de una vez — usar paginación por cursor, especialmente en discusiones activas
- Solo análisis de menciones del lado del cliente — validar y extraer menciones del lado del servidor para notificaciones
- Eliminación dura de comentarios con respuestas — esto huérfana el hilo; usar borrado suave y mostrar marcador de posición
- Reacciones sin restricciones únicas — permite reacciones duplicadas del mismo usuario