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:
| Property | Type | Description |
|---|
messages | UIMessage[] | Real-time messages with streaming status |
status | AgentStatus | Convex connection status |
sendMessage(content) | Promise<void> | Send a message |
isStreaming | boolean | AI is currently generating |
isLoadingRoom | boolean | Room is being loaded |
roomError | unknown | Error if room failed to load |
Why Convex?
- Real-time by default — queries update automatically when data changes
- Optimistic updates — mutations update UI immediately
- Agent integration —
useUIMessages with stream: true for AI streaming
- No provider setup needed —
ConvexProvider 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 Case | Hook |
|---|
| Real-time chat messages | useConvexGroupChat |
| LMS courses & enrollments | TanStack Query hooks (useCourses, etc.) |
| Vicky AI chat | useVickyChat |
| Conversation starters | useConversationStarter |
| Local persisted data | useMoments (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