Skip to main content

Overview

Synto Mobile uses custom React hooks to encapsulate complex logic and promote code reuse. Hooks are organized in the /hooks directory and provide clean APIs for common functionality.

Available hooks

hooks/
hooks/
├── use-agent.tsx           # Solana agent for AI-powered blockchain actions
├── use-color-scheme.ts     # Theme detection
├── use-multi-chat.tsx      # Multi-chat conversation management
├── use-require-auth.tsx    # Authentication guard
├── use-theme-color.ts      # Theme color extraction
└── use-track-locations.ts  # Route tracking for analytics

Authentication hooks

useAuth

Core authentication hook provided by AuthProvider. Location: components/auth/auth-provider.tsx
export interface AuthState {
  isAuthenticated: boolean;
  isLoading: boolean;
  hasCompletedOnboarding: boolean;
  signIn: () => Promise<Account>;
  signOut: () => Promise<void>;
  markOnboardingComplete: () => Promise<void>;
  checkAuthAndRedirect: () => void;
}

export function useAuth(): AuthState {
  const value = use(Context);
  if (!value) {
    throw new Error("useAuth must be wrapped in a <AuthProvider />");
  }
  return value;
}
function MyComponent() {
  const { isAuthenticated, isLoading, signIn, signOut } = useAuth();
  
  if (isLoading) {
    return <ActivityIndicator />;
  }
  
  if (!isAuthenticated) {
    return <Button onPress={signIn}>Connect Wallet</Button>;
  }
  
  return (
    <View>
      <Text>Connected!</Text>
      <Button onPress={signOut}>Disconnect</Button>
    </View>
  );
}
The checkAuthAndRedirect function is called automatically by the AuthProvider. You rarely need to call it manually.

useRequireAuth

Convenience hook for protected screens that require authentication. Location: hooks/use-require-auth.tsx
export function useRequireAuth() {
  const { isAuthenticated, isLoading, checkAuthAndRedirect } = useAuth();
  
  useEffect(() => {
    if (!isLoading) {
      checkAuthAndRedirect();
    }
  }, [isAuthenticated, isLoading, checkAuthAndRedirect]);
  
  return {
    isAuthenticated,
    isLoading,
    isReady: !isLoading && isAuthenticated,
  };
}
function ProtectedScreen() {
  const { isReady } = useRequireAuth();
  
  if (!isReady) {
    return <LoadingSpinner />;
  }
  
  return <YourProtectedContent />;
}

Solana blockchain hooks

useSolanaAgent

Creates an AI-powered Solana agent with blockchain action capabilities. Location: hooks/use-agent.tsx
export function useSolanaAgent() {
  const { signAndSendTransaction, signTransactions, signMessage } = useMobileWallet();
  const { account } = useWalletUi();
  
  const agent = useMemo(() => {
    if (!account?.publicKey) return null;
    
    const fns = {
      signTransaction: async <T extends Transaction | VersionedTransaction>(tx: T): Promise<T> => {
        if (tx instanceof Transaction) {
          throw new Error(
            "Legacy Transaction signing not supported. Use VersionedTransaction instead."
          );
        }
        const signed = await signTransactions([tx as VersionedTransaction]);
        return signed[0] as T;
      },
      signMessage: async (msg: Uint8Array) => {
        return await signMessage(msg);
      },
      sendTransaction: async <T extends Transaction | VersionedTransaction>(tx: T) => {
        return await signAndSendTransaction(tx);
      },
      signAllTransactions: async <T extends Transaction | VersionedTransaction>(txs: T[]) => {
        const signed = await signTransactions(txs as VersionedTransaction[]);
        return signed as T[];
      },
      publicKey: account.publicKey,
    };
    
    return agentBuilder(fns, { signOnly: false });
  }, [account, signAndSendTransaction, signTransactions, signMessage]);
  
  const tools = useMemo(() => {
    if (agent) {
      return createVercelAITools(agent, agent.actions);
    }
    return null;
  }, [agent]);
  
  return { agent, tools, isReady: !!agent };
}
Important: The agent is only available when a wallet is connected. Always check isReady before using the agent or tools.

Return values

agent
SolanaAgentKit | null
The agent instance with all registered actions and plugins. null if no wallet is connected.
tools
VercelAITools | null
Tools formatted for Vercel AI SDK integration. Used in chat functionality.
isReady
boolean
true when the agent is fully initialized and ready to use.

Available agent actions

The agent comes with pre-configured plugins:
  • GET_BALANCE - Get SOL balance
  • GET_BALANCE_OTHER - Get balance of other wallets
  • GET_TOKEN_BALANCES - List all token holdings
  • TRANSFER - Send SOL or tokens
  • REQUEST_FAUCET_FUNDS - Get devnet/testnet airdrop
  • GET_TPS - Current network TPS
  • CLOSE_EMPTY_TOKEN_ACCOUNTS - Clean up empty token accounts
  • FETCH_PRICE - Get token prices
  • GET_TOKEN_DATA_BY_TICKER - Search tokens by symbol
  • TRADE - Execute token swaps
  • STAKE_WITH_JUP - Stake with Jupiter
  • GET_TOKEN_DATA - Fetch comprehensive token data and metrics
  • RAINFI_TRANSACTION_BUILDER - Build RainFi lending transactions
  • QUOTE_LOAN_CALCULATOR - Calculate loan quotes
  • GET_USER_LOANS - Fetch user’s active loans
  • REPAY_LOAN - Repay lending positions

useMobileWallet

Low-level wallet interaction hook. Location: components/solana/use-mobile-wallet.tsx
export function useMobileWallet() {
  return {
    connect: () => Promise<Account>,
    signIn: (payload: SignInPayload) => Promise<Account>,
    disconnect: () => Promise<void>,
    signAndSendTransaction: (tx: Transaction | VersionedTransaction) => Promise<TransactionSignature>,
    signMessage: (message: Uint8Array) => Promise<Uint8Array>,
    signTransactions: (txs: VersionedTransaction[]) => Promise<VersionedTransaction[]>,
  };
}
function SendTransaction() {
  const { signAndSendTransaction } = useMobileWallet();
  const { account } = useWalletUi();
  
  const handleSend = async () => {
    const transaction = new VersionedTransaction(/* ... */);
    const signature = await signAndSendTransaction(transaction);
    console.log('Transaction sent:', signature);
  };
  
  return <Button onPress={handleSend}>Send</Button>;
}
The hook includes automatic error handling that disconnects the wallet on authorization errors.

Account query hooks

These hooks use TanStack Query for automatic caching and refetching.
Location: components/account/use-get-balance.tsx
export function useGetBalance({ address }: { address: PublicKey }) {
  const connection = useConnection();
  const queryKey = useGetBalanceQueryKey({ 
    address, 
    endpoint: connection.rpcEndpoint 
  });
  
  return useQuery({
    queryKey,
    queryFn: () => connection.getBalance(address),
  });
}

export function useGetBalanceInvalidate({ address }: { address: PublicKey }) {
  const connection = useConnection();
  const queryKey = useGetBalanceQueryKey({ address, endpoint: connection.rpcEndpoint });
  const client = useQueryClient();
  
  return () => client.invalidateQueries({ queryKey });
}
Usage:
function BalanceDisplay() {
  const { account } = useWalletUi();
  const { data: balance, isLoading } = useGetBalance({ 
    address: account.publicKey 
  });
  
  if (isLoading) return <Skeleton />;
  
  return <Text>{balance / LAMPORTS_PER_SOL} SOL</Text>;
}
Location: components/account/use-get-token-accounts.tsxFetches all SPL token accounts for a given address.
export function useGetTokenAccounts({ address }: { address: PublicKey }) {
  const connection = useConnection();
  
  return useQuery({
    queryKey: ['get-token-accounts', { endpoint: connection.rpcEndpoint, address }],
    queryFn: async () => {
      const accounts = await connection.getParsedTokenAccountsByOwner(
        address,
        { programId: TOKEN_PROGRAM_ID }
      );
      return accounts.value;
    },
  });
}
Location: components/account/use-transfer-sol.tsxMutation hook for sending SOL.
function SendSol() {
  const { account } = useWalletUi();
  const { mutate: transfer, isPending } = useTransferSol({ 
    address: account.publicKey 
  });
  
  const handleSend = () => {
    transfer({
      destination: new PublicKey('...'),
      amount: 0.1 * LAMPORTS_PER_SOL,
    });
  };
  
  return (
    <Button onPress={handleSend} disabled={isPending}>
      {isPending ? 'Sending...' : 'Send 0.1 SOL'}
    </Button>
  );
}
Location: components/account/use-request-airdrop.tsxRequest devnet/testnet airdrop.
function AirdropButton() {
  const { account } = useWalletUi();
  const { mutate: requestAirdrop, isPending } = useRequestAirdrop({ 
    address: account.publicKey 
  });
  
  return (
    <Button 
      onPress={() => requestAirdrop(1)} 
      disabled={isPending}
    >
      Request 1 SOL Airdrop
    </Button>
  );
}

Chat management hooks

useMultiChat

Manages multiple chat conversations with persistence. Location: hooks/use-multi-chat.tsx
interface UseMultiChatReturn {
  // Current chat state
  currentChat: Chat | null;
  currentChatId: string | null;
  
  // Chat list
  chatList: ChatMetadata[];
  
  // Actions
  createNewChat: (firstMessage?: string) => Promise<Chat>;
  loadChat: (chatId: string) => Promise<void>;
  deleteChat: (chatId: string) => Promise<void>;
  updateChatTitle: (chatId: string, title: string) => Promise<void>;
  syncMessages: (messages: Message[]) => Promise<void>;
  
  // Loading states
  isLoadingChat: boolean;
  isLoadingList: boolean;
}

export function useMultiChat(): UseMultiChatReturn {
  // Implementation
}
function ChatInterface() {
  const {
    currentChat,
    chatList,
    createNewChat,
    loadChat,
    syncMessages,
  } = useMultiChat();
  
  const { messages } = useChat({
    initialMessages: currentChat?.messages || [],
  });
  
  // Sync messages to storage
  useEffect(() => {
    if (messages.length > 0 && currentChat) {
      syncMessages(messages);
    }
  }, [messages, currentChat, syncMessages]);
  
  return <ChatUI messages={messages} />;
}
Key features:
  • Automatic persistence using AsyncStorage via ChatStorageService
  • Auto-generates chat titles from first message
  • Tracks last active chat
  • Handles chat deletion with automatic fallback
  • Syncs messages from AI SDK to storage
The syncMessages function is debounced internally to avoid excessive writes. It’s safe to call on every message update.

Cluster & network hooks

useCluster

Manages Solana network selection. Location: components/cluster/cluster-provider.tsx
export interface ClusterProviderContext {
  selectedCluster: Cluster;
  clusters: Cluster[];
  setSelectedCluster: (cluster: Cluster) => void;
  getExplorerUrl(path: string): string;
}

export function useCluster(): ClusterProviderContext {
  return useContext(Context);
}
function NetworkSelector() {
  const { selectedCluster, clusters, setSelectedCluster } = useCluster();
  
  return (
    <Picker
      selectedValue={selectedCluster.id}
      onValueChange={(id) => {
        const cluster = clusters.find(c => c.id === id);
        if (cluster) setSelectedCluster(cluster);
      }}
    >
      {clusters.map(cluster => (
        <Picker.Item 
          key={cluster.id} 
          label={cluster.name} 
          value={cluster.id} 
        />
      ))}
    </Picker>
  );
}

useConnection

Accesses the current Solana RPC connection. Location: components/solana/solana-provider.tsx
export function useConnection(): Connection {
  return useSolana().connection;
}
Usage:
function GetSlot() {
  const connection = useConnection();
  const [slot, setSlot] = useState<number>();
  
  useEffect(() => {
    connection.getSlot().then(setSlot);
  }, [connection]);
  
  return <Text>Current slot: {slot}</Text>;
}
The connection instance changes when the cluster is switched. Use it in useMemo or useEffect dependencies.

Theme hooks

useColorScheme

Detects the device’s color scheme (light/dark). Location: hooks/use-color-scheme.ts
export { useColorScheme } from "react-native";
Usage:
function ThemedComponent() {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === 'dark';
  
  return (
    <View style={{ backgroundColor: isDark ? '#000' : '#fff' }}>
      <Text style={{ color: isDark ? '#fff' : '#000' }}>
        Current theme: {colorScheme}
      </Text>
    </View>
  );
}

useAppTheme

Provides React Navigation theme based on color scheme. Location: components/app-theme.tsx
export function useAppTheme() {
  const colorScheme = useColorScheme();
  const isDark = colorScheme === "dark";
  const theme = isDark ? AppThemeDark : AppThemeLight;
  
  return { colorScheme, isDark, theme };
}

Analytics hooks

useTrackLocations

Tracks route changes for analytics. Location: hooks/use-track-locations.ts
export function useTrackLocations(
  onChange: (pathname: string, params: UnknownOutputParams) => void
) {
  const pathname = usePathname();
  const params = useGlobalSearchParams();
  
  useEffect(() => {
    onChange(pathname, params);
  }, [onChange, pathname, params]);
}
Usage:
app/_layout.tsx
export default function RootLayout() {
  useTrackLocations((pathname, params) => {
    console.log(`Track ${pathname}`, { params });
    // Send to analytics service
    analytics.track('page_view', { pathname, ...params });
  });
  
  return <RootNavigator />;
}

Best practices

Always include all dependencies in useEffect, useMemo, and useCallback:
// ✅ Good
const agent = useMemo(() => {
  return agentBuilder(fns, config);
}, [fns, config]);

// ❌ Bad
const agent = useMemo(() => {
  return agentBuilder(fns, config);
}, []); // Missing dependencies!
Always handle errors in async hooks:
const { mutate: transfer } = useTransferSol({ address });

const handleSend = async () => {
  try {
    await transfer({ destination, amount });
    Alert.alert('Success');
  } catch (error) {
    Alert.alert('Error', error.message);
  }
};
Show loading indicators for async operations:
const { data, isLoading, error } = useGetBalance({ address });

if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <BalanceDisplay balance={data} />;

Next steps

Component structure

Learn about component organization

Building the app

Build and deployment guide

Build docs developers (and LLMs) love