Hooks and Data Fetching

Status: TanStack Query hooks are defined in lib/api-client.ts but not yet integrated into components. Convex handles all active data fetching and real-time updates.

Data Fetching Architecture

Nous uses Convex for real-time data and TanStack Query for API caching (LMS endpoints):

Convex (Primary Data Layer)

Convex handles all real-time data for the app — messages, matches, group chat, and AI agents.

useConvexGroupChat

apps/mobile/src/hooks/use-convex-group-chat.ts Real-time group chat with Convex Agent streaming:
import { useConvexGroupChat } from '~/hooks/use-convex-group-chat'

function ChatScreen({ matchId }: { matchId: string }) {
  const {
    messages,        // Real-time message list with streaming status
    status,          // Connection status
    sendMessage,     // Send a message
    isStreaming,      // AI is generating
    roomError,        // Error loading room
  } = useConvexGroupChat({
    matchId,
    currentUserName: 'Alice',
    matchUserId: 'user_bob',
    matchUserName: 'Bob',
  })

  // ...
}
Returns:
PropertyTypeDescription
messagesUIMessage[]Real-time messages with streaming status
statusAgentStatusConvex connection status
sendMessage(content)Promise<void>Send a message
isStreamingbooleanAI is currently generating
isLoadingRoombooleanRoom is being loaded
roomErrorunknownError if room failed to load

Why Convex?

  • Real-time by default — queries update automatically when data changes
  • Optimistic updates — mutations update UI immediately
  • Agent integrationuseUIMessages with stream: true for AI streaming
  • No provider setup neededConvexProvider wraps the app

TanStack Query (LMS API)

apps/mobile/src/lib/api-client.ts TanStack Query hooks wrap LMS API endpoints from Mastra. These are defined but not yet integrated into components.

Available Hooks

import {
  useCourses,           // GET /lms/courses
  useCourse,            // GET /lms/courses/:id
  useEnrollments,       // GET /lms/enrollments/:userId
  useEnroll,            // POST /lms/enroll
  useUpdateProgress,     // POST /lms/progress
  useVideoUrl,          // GET /lms/video-url/:lessonId
} from '~/lib/api-client'

useCourses

const { data, isLoading, error } = useCourses()
// data: Course[] | undefined

useCourse

const { data, isLoading, error } = useCourse(courseId)
// enabled: !!courseId (won't fetch if courseId is empty)

useEnrollments

const { data, isLoading, error } = useEnrollments(userId)
// enabled: !!userId

useEnroll (Mutation)

const { mutate, isPending } = useEnroll()
mutate({ courseId: 'course_123', userId: 'user_alice' })
// Automatically invalidates useEnrollments on success

useUpdateProgress (Mutation)

const { mutate, isPending } = useUpdateProgress()
mutate({ lessonId: 'lesson_123', watchedSeconds: 120, userId: 'user_alice' })

useVideoUrl

const { data, isLoading, error } = useVideoUrl(lessonId)
// data: VideoUrl | undefined

AI SDK Hooks (Streaming)

These hooks use the AI SDK for streaming responses from Mastra agents.

useVickyChat

apps/mobile/src/hooks/use-vicky-chat.ts Vicky AI wingman chat with streaming:
import { useVickyChat } from '~/hooks/use-vicky-chat'

function VickyChatScreen() {
  const { messages, sendMessage, isLoading, isAiEnabled, setIsAiEnabled } = useVickyChat()

  const handleSend = (content: string) => {
    sendMessage(content)
  }

  return (
    <>
      {messages.map(msg => (
        <MessageBubble key={msg.id} message={msg} />
      ))}
      <MessageInput onSend={handleSend} disabled={isLoading} />
    </>
  )
}

useChatStream

apps/mobile/src/hooks/use-chat-stream.ts Generic Nous AI chat:
import { useChatStream } from '~/hooks/use-chat-stream'

function NousChatScreen() {
  const { messages, sendMessage, isLoading } = useChatStream({
    onError: (error) => consola.error('Chat error', { error }),
  })
  // ...
}

useConversationStarter

apps/mobile/src/hooks/use-conversation-starter.ts AI-generated conversation openers:
import { useConversationStarter } from '~/hooks/use-conversation-starter'

const { suggestions, isLoading, error, generate, clear } = useConversationStarter()

// Generate suggestions for a match
await generate('Match Name', 'shared interest')
// suggestions: string[]

Service Layer (Direct Fetch)

Services in services/ make direct fetch calls without TanStack Query caching:

lms-service.ts

apps/mobile/src/services/lms-service.ts
import { getPublishedCourses, enrollInCourse } from '~/services/lms-service'

// Direct fetch - no caching
const courses = await getPublishedCourses()
await enrollInCourse(userId, courseId)

matches-service.ts

apps/mobile/src/services/matches-service.ts
import { getMatches, getMessages, sendMessage } from '~/services/matches-service'

const matches = await getMatches(userId)
const messages = await getMessages(matchId)
await sendMessage(matchId, content, userId)
Note: These services are used directly without caching. For production, prefer the TanStack Query hooks (once integrated) for automatic caching and background refetching.

Local Storage Hooks

useMoments

apps/mobile/src/hooks/use-moments.ts Persisted moments using AsyncStorage:
import { useMoments } from '~/hooks/use-moments'

const { moments, saveMoment, isLoading } = useMoments()

await saveMoment({
  id: 'moment_123',
  questionId: 'q_1',
  question: 'What makes you laugh?',
  videoUri: 'file://...',
  createdAt: new Date().toISOString(),
})

Hook Pattern Guidelines

When to Use Which

Use CaseHook
Real-time chat messagesuseConvexGroupChat
LMS courses & enrollmentsTanStack Query hooks (useCourses, etc.)
Vicky AI chatuseVickyChat
Conversation startersuseConversationStarter
Local persisted datauseMoments (AsyncStorage)

Avoid

  • Fern for AI — use AI SDK hooks instead
  • Direct fetch in render — use TanStack Query or Convex
  • Multiple copies of state — prefer Convex real-time over local state

TanStack Query Provider Setup

TanStack Query requires a QueryClientProvider wrapper. Currently, only ConvexProvider is set up in app/_layout.tsx. To fully enable TanStack Query:
// app/_layout.tsx (not yet done)
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      retry: 2,
    },
  },
})

export default function RootLayout() {
  return (
    <QueryClientProvider client={queryClient}>
      {/* existing providers */}
    </QueryClientProvider>
  )
}

Migration: Services → TanStack Query

When integrating TanStack Query hooks, replace direct fetch calls:
// BEFORE: Direct fetch in component
const [courses, setCourses] = useState([])
useEffect(() => {
  getPublishedCourses().then(setCourses)
}, [])

// AFTER: TanStack Query hook
const { data: courses } = useCourses()

Last Updated: 2026-05-14