Documentation Index Fetch the complete documentation index at: https://mintlify.com/egeuysall/ryva-archive/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Ryva uses a layered state management approach:
TanStack Query (React Query) - Server state, data fetching, caching
Zustand - Client-side global state (minimal usage)
React Context - Component tree state sharing
React Hook Form - Form state management
TanStack Query (React Query)
Setup and Configuration
Query client is configured in src/lib/query-client.ts:
Query Client Configuration
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
const defaultQueryOptions = {
queries: {
staleTime: 5 * 60 * 1000 , // 5 minutes
gcTime: 10 * 60 * 1000 , // 10 minutes (formerly cacheTime)
retry: 1 ,
retryDelay : ( attemptIndex : number ) => Math . min ( 1000 * 2 ** attemptIndex , 30000 ),
refetchOnWindowFocus: process . env . NODE_ENV === 'production' ,
refetchOnReconnect: true ,
refetchOnMount: false ,
},
mutations: {
retry: 1 ,
},
}
export function createQueryClient () : QueryClient {
return new QueryClient ({
defaultOptions: defaultQueryOptions ,
})
}
let browserQueryClient : QueryClient | undefined = undefined
export function getQueryClient () : QueryClient {
// Server: always create new client
if ( typeof window === 'undefined' ) {
return createQueryClient ()
}
// Browser: reuse existing client
if ( ! browserQueryClient ) {
browserQueryClient = createQueryClient ()
}
return browserQueryClient
}
Provider Setup
The QueryClientProvider wraps the app in src/lib/providers.tsx:
'use client'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { getQueryClient } from './query-client'
export function Providers ({ children } : { children : ReactNode }) {
const queryClient = getQueryClient ()
return (
< QueryClientProvider client = { queryClient } >
{ children }
{ process . env . NODE_ENV === ' development ' && (
< ReactQueryDevtools initialIsOpen = { false } buttonPosition = "bottom-right" />
)}
</ QueryClientProvider >
)
}
Query Patterns
1. Query Keys Organization
Query keys are organized hierarchically:
// src/modules/auth/hooks/use-auth-api.ts
export const authKeys = {
me: [ 'auth' , 'me' ] as const ,
preferences: [ 'auth' , 'preferences' ] as const ,
}
// src/lib/query-client.ts
export const queryKeys = {
users: {
all: [ 'users' ] as const ,
lists : () => [ ... queryKeys . users . all , 'list' ] as const ,
list : ( filters : Record < string , unknown >) => [ ... queryKeys . users . lists (), filters ] as const ,
details : () => [ ... queryKeys . users . all , 'detail' ] as const ,
detail : ( id : string ) => [ ... queryKeys . users . details (), id ] as const ,
},
} as const
Pattern : ['resource', 'type', ...params] allows for granular cache invalidation.
2. useQuery Hook Pattern
Standard query hook implementation:
Basic Query
Conditional Query
With Options
import { useQuery } from '@tanstack/react-query'
import { apiClient } from '@/lib/api/client'
export function useUser ( options ?: { enabled ?: boolean }) {
return useQuery ({
queryKey: authKeys . me ,
queryFn : async () => {
const res = await apiClient . get < GetMeResponse >( '/v1/auth/me' )
if ( ! res . success || ! res . data ) {
throw new Error ( res . error ?. message || 'Failed to fetch user' )
}
return res . data
},
retry: false ,
enabled: options ?. enabled ?? true ,
})
}
// Usage in component
function UserProfile () {
const { data : user , isLoading , error } = useUser ()
if ( isLoading ) return < Skeleton />
if ( error ) return < Alert > Error loading user </ Alert >
return < div >{user. name } </ div >
}
export function useListPendingInvitations ( organizationId : string ) {
return useQuery ({
queryKey: [ ORGANIZATIONS_KEY , organizationId , INVITATIONS_KEY ],
queryFn : async () => {
const response = await apiClient . get < ListPendingInvitationsResponse >(
`/v1/organizations/ ${ organizationId } /invitations`
)
return response . data
},
enabled: !! organizationId , // Only run when organizationId exists
})
}
export function usePreferences () {
return useQuery ({
queryKey: authKeys . preferences ,
queryFn : async () => {
const res = await apiClient . get < GetPreferencesResponse >( '/v1/auth/preferences' )
if ( ! res . success || ! res . data ) {
throw new Error ( res . error ?. message || 'Failed to fetch preferences' )
}
return res . data
},
staleTime: 10 * 60 * 1000 , // 10 minutes (override default)
gcTime: 30 * 60 * 1000 , // 30 minutes
})
}
3. useMutation Hook Pattern
Mutations for data modifications:
import { useMutation , useQueryClient } from '@tanstack/react-query'
export function useCreateOrganization () {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : async ( data : CreateOrganizationRequest ) => {
const response = await apiClient . post < CreateOrganizationResponse >(
'/v1/organizations' ,
data
)
return response . data
},
onSuccess : () => {
// Invalidate and refetch user data (includes organizations list)
setTimeout (() => {
queryClient . invalidateQueries ({ queryKey: authKeys . me })
}, 500 )
},
})
}
// Usage in component
function CreateOrgForm () {
const createOrg = useCreateOrganization ()
const handleSubmit = async ( data : FormData ) => {
try {
await createOrg . mutateAsync ( data )
toast . success ( 'Organization created!' )
} catch ( error ) {
toast . error ( 'Failed to create organization' )
}
}
return (
< form onSubmit = { handleSubmit } >
< Button type = "submit" disabled = {createOrg. isPending } >
{ createOrg . isPending ? 'Creating...' : 'Create' }
</ Button >
</ form >
)
}
export function useUpdateProfile () {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : async ( data : UpdateProfileRequest ) => {
const res = await apiClient . patch < UpdateProfileResponse >(
'/v1/auth/profile' ,
data
)
if ( ! res . success || ! res . data ) {
throw new Error ( res . error ?. message || 'Failed to update profile' )
}
return res . data
},
onSuccess : () => {
// Invalidate user query to refetch with updated data
queryClient . invalidateQueries ({ queryKey: authKeys . me })
},
})
}
export function useCancelInvitation () {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : async ({ organizationId , invitationId }) => {
await apiClient . delete (
`/v1/organizations/ ${ organizationId } /invitations/ ${ invitationId } `
)
},
onSuccess : ( _data , variables ) => {
// Invalidate only the specific invitations list
setTimeout (() => {
queryClient . invalidateQueries ({
queryKey: [ ORGANIZATIONS_KEY , variables . organizationId , INVITATIONS_KEY ],
})
}, 100 )
},
})
}
4. Cache Invalidation Patterns
// Invalidate exact query
queryClient . invalidateQueries ({
queryKey: authKeys . me
})
// Invalidate all queries starting with ['organizations']
queryClient . invalidateQueries ({
queryKey: [ ORGANIZATIONS_KEY ]
})
onSuccess : () => {
setTimeout (() => {
queryClient . invalidateQueries ({ queryKey: [ USER_INVITATIONS_KEY ] })
queryClient . invalidateQueries ({ queryKey: authKeys . me })
}, 100 )
}
Timeout Pattern : The codebase uses setTimeout delays (100-500ms) after mutations to avoid immediate CORS preflight issues in development with the Next.js proxy.
API Client
Custom HTTP client in src/lib/api/client.ts:
import { createClient } from '@/lib/supabase/client'
import type { APIResponse } from './types'
class APIClient {
private baseURL : string
constructor () {
this . baseURL = siteConfig . apiUrl
}
private async getAuthToken () : Promise < string | null > {
const supabase = createClient ()
const { data : { session } } = await supabase . auth . getSession ()
return session ?. access_token ?? null
}
async request < T >( endpoint : string , options : RequestInit = {}) : Promise < APIResponse < T >> {
const token = await this . getAuthToken ()
const headers : Record < string , string > = {
'Content-Type' : 'application/json' ,
... ( options . headers as Record < string , string >),
}
if ( token ) {
headers [ 'Authorization' ] = `Bearer ${ token } `
}
// Request timeout handling
const controller = new AbortController ()
const timeoutId = setTimeout (() => controller . abort (), 30000 )
try {
const response = await fetch ( ` ${ this . baseURL }${ endpoint } ` , {
... options ,
headers ,
signal: controller . signal ,
cache: 'no-store' ,
})
clearTimeout ( timeoutId )
// Handle 204 No Content
if ( response . status === 204 ) {
return { success: true , data: null as T }
}
const data = await response . json ()
if ( ! response . ok ) throw data
return data as APIResponse < T >
} catch ( error ) {
clearTimeout ( timeoutId )
if ( error . name === 'AbortError' ) {
throw new Error ( 'Request timeout - please try again' )
}
throw error
}
}
async get < T >( endpoint : string ) { return this . request < T >( endpoint , { method: 'GET' }) }
async post < T >( endpoint : string , body ?: unknown ) {
return this . request < T >( endpoint , { method: 'POST' , body: JSON . stringify ( body ) })
}
async patch < T >( endpoint : string , body ?: unknown ) {
return this . request < T >( endpoint , { method: 'PATCH' , body: JSON . stringify ( body ) })
}
async put < T >( endpoint : string , body ?: unknown ) {
return this . request < T >( endpoint , { method: 'PUT' , body: JSON . stringify ( body ) })
}
async delete < T >( endpoint : string ) { return this . request < T >( endpoint , { method: 'DELETE' }) }
}
export const apiClient = new APIClient ()
Zustand Stores
Minimal client state management for non-server data.
Auth Store
Session user state (src/stores/auth.ts):
import { create } from 'zustand'
import type { User as SupabaseUser } from '@supabase/supabase-js'
interface AuthState {
user : SupabaseUser | null
isLoading : boolean
setUser : ( user : SupabaseUser | null ) => void
setLoading : ( loading : boolean ) => void
logout : () => void
}
export const useAuthStore = create < AuthState >( set => ({
user: null ,
isLoading: true ,
setUser : user => set ({ user , isLoading: false }),
setLoading : isLoading => set ({ isLoading }),
logout : () => set ({ user: null }),
}))
// Usage
function UserBadge () {
const user = useAuthStore ( state => state . user )
return < div >{user?. email } </ div >
}
Waitlist Store (Persisted)
Local storage persistence with middleware (src/stores/waitlist.ts):
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface WaitlistState {
hasJoined : boolean
email : string | null
position : number | null
setJoined : ( email : string , position : number ) => void
reset : () => void
}
export const useWaitlistStore = create < WaitlistState >()(
persist (
set => ({
hasJoined: false ,
email: null ,
position: null ,
setJoined : ( email , position ) => set ({ hasJoined: true , email , position }),
reset : () => set ({ hasJoined: false , email: null , position: null }),
}),
{
name: 'ryva-waitlist' , // localStorage key
}
)
)
When to use Zustand : UI state (modals, sidebar open/closed), temporary client data, or state that doesn’t belong on the server.
React Context
Organization Context
Active organization selection (src/contexts/organization-context.tsx):
Organization Context Implementation
'use client'
import { createContext , useContext , useState , useEffect , useMemo , useCallback } from 'react'
import { useUser } from '@/modules/auth/hooks/use-auth-api'
import type { OrganizationMembership } from '@/lib/api/types'
interface OrganizationContextValue {
activeOrganization : OrganizationMembership | null
setActiveOrganization : ( org : OrganizationMembership | null ) => void
organizations : OrganizationMembership []
isLoading : boolean
subdomain : string | null
}
const OrganizationContext = createContext < OrganizationContextValue | undefined >( undefined )
const ACTIVE_ORG_KEY = 'ryva:active-org-id'
export function OrganizationProvider ({ children } : { children : React . ReactNode }) {
const { data : user , isLoading : isUserLoading } = useUser ()
const [ activeOrganization , setActiveOrganizationState ] = useState < OrganizationMembership | null >( null )
const subdomain = useMemo (() => getSubdomain (), [])
// Memoize organizations to prevent re-renders
const organizations = useMemo (() => user ?. organizations || [], [ user ])
// Initialize state based on subdomain and localStorage
useEffect (() => {
if ( isUserLoading || ! user ) return
if ( subdomain ) {
const matchedOrg = organizations . find (
org => org . organization_slug ?. toLowerCase () === subdomain . toLowerCase ()
)
if ( matchedOrg ) {
setActiveOrganizationState ( matchedOrg )
localStorage . setItem ( ACTIVE_ORG_KEY , matchedOrg . organization_id )
return
}
}
// Fallback to localStorage or first org
const savedOrgId = localStorage . getItem ( ACTIVE_ORG_KEY )
const savedOrg = organizations . find ( org => org . organization_id === savedOrgId )
if ( savedOrg ) {
setActiveOrganizationState ( savedOrg )
} else if ( organizations . length > 0 ) {
setActiveOrganizationState ( organizations [ 0 ])
localStorage . setItem ( ACTIVE_ORG_KEY , organizations [ 0 ]. organization_id )
}
}, [ user , organizations , isUserLoading , subdomain ])
const setActiveOrganization = useCallback (( org : OrganizationMembership | null ) => {
if ( org === null ) {
setActiveOrganizationState ( null )
localStorage . removeItem ( ACTIVE_ORG_KEY )
} else {
setActiveOrganizationState ( org )
localStorage . setItem ( ACTIVE_ORG_KEY , org . organization_id )
}
}, [])
const value = {
activeOrganization ,
setActiveOrganization ,
organizations ,
isLoading: isUserLoading ,
subdomain ,
}
return < OrganizationContext . Provider value ={ value }>{ children } </ OrganizationContext . Provider >
}
export function useOrganization () {
const context = useContext ( OrganizationContext )
if ( context === undefined ) {
throw new Error ( 'useOrganization must be used within an OrganizationProvider' )
}
return context
}
Usage:
function TeamSwitcher () {
const { activeOrganization , setActiveOrganization , organizations } = useOrganization ()
return (
< Select value = {activeOrganization?. id } onValueChange = { handleChange } >
{ organizations . map ( org => (
< SelectItem key = {org. id } value = {org. id } >
{ org . name }
</ SelectItem >
))}
</ Select >
)
}
Best Practices
Prefer TanStack Query for Server State
Use TanStack Query for all data from APIs. It handles caching, revalidation, and loading states automatically.
Keep Query Keys Consistent
Define query keys in module files alongside the hooks. Use hierarchical keys for easy invalidation.
Only use Zustand for true client state (UI preferences, temporary data). Server data should use TanStack Query.
Invalidate Queries After Mutations
Always invalidate related queries in onSuccess callbacks to keep the UI in sync.
Use Optimistic Updates Sparingly
Only implement optimistic updates for simple mutations where rollback is easy.
Handle Loading and Error States
Always render appropriate UI for isLoading, error, and empty states.
State Management Decision Tree
Memoize Expensive Computations Use useMemo for derived data and useCallback for stable function references.
Configure Stale Time Set appropriate staleTime to reduce unnecessary refetches.
Selective Query Invalidation Invalidate only affected queries, not entire query prefixes.
Enable Query Devtools Use React Query Devtools in development to debug cache behavior.
Next Steps
Component Patterns Learn about component architecture
Frontend Structure Review the project organization