API & Networking

Twinbloc provides a robust networking layer built on Axios, TanStack Query, and React Query Kit. It handles authentication, error parsing, and offline persistence out of the box.

The Client Wrapper

We use a custom wrapper executeRest around Axios to standardize request handling. This function automatically:

  • Injects the Authorization header with the current access token.
  • Handles 401 Unauthorized errors by signing the user out.
  • Unwraps the response data.
  • Provides typed error handling via ApiError.
1import { executeRest } from '@/api/common';
2
3// Basic GET request
4const data = await executeRest<User>('/users/me', 'GET');
5
6// POST request with body
7await executeRest('/auth/login', 'POST', {
8 email: 'user@example.com',
9 password: 'password'
10});
11
12// Request with custom options (headers, params)
13await executeRest('/search', 'GET', undefined, {
14 params: { q: 'react' },
15 ignore401: true // Don't sign out on 401
16});

Reusable Query Hooks

We use react-query-kit to create reusable query hooks. This keeps your API logic co-located and type-safe.

Here is an example of the useGetUser hook located in src/api/user/use-get-user.ts:

1import { createQuery } from "react-query-kit";
2import { executeRest, QueryKey } from "../common";
3import { type TUser } from "./types";
4
5// 1. Create the base query
6const _useGetUser = createQuery<TUser, void, AxiosError>({
7 queryKey: [QueryKey.USER], // unique key for caching
8 fetcher: async () => {
9 const response = await executeRest<{ data: TUser }>("auth/me", "GET");
10 return response?.data;
11 },
12});
13
14// 2. Export a custom hook wrapper for better DX
15export const useGetUser = (userId?: string) => {
16 const { auth_data } = useAuth();
17
18 // Only run query if user is logged in
19 return _useGetUser({
20 enabled: !!auth_data?.access,
21 });
22};

Pagination & Infinite Lists

For infinite scrolling lists (like feeds), we provide utilities in api-utils.ts to normalize pages and handle cursors.

1import { createInfiniteQuery } from "react-query-kit";
2import { getNextPageParam, normalizePages } from "@/api/common";
3
4const usePosts = createInfiniteQuery<Post[]>({
5 queryKey: ['posts'],
6 fetcher: async ({ pageParam }) => {
7 return executeRest('/posts', 'GET', undefined, {
8 params: { cursor: pageParam }
9 });
10 },
11 getNextPageParam, // Automatically extracts 'next', 'cursor', etc.
12});
13
14// Usage in component
15const { data } = usePosts();
16const allPosts = normalizePages(data?.pages); // Flattens pages into single array

Persistence & Provider

The APIProvider in src/api/common/api-provider.tsx wraps the app with the QueryClient and configures persistence using MMKV.

1import { APIProvider } from '@/api/common';
2
3export default function App() {
4 return (
5 <APIProvider>
6 <RootNavigator />
7 </APIProvider>
8 );
9}

Note: Queries are persisted to MMKV storage by default. You can control this per-query using gcTime and staleTime.

Last updated on 2/10/2026

Edit this page on GitHub