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
Authorizationheader with the current access token. - Handles 401 Unauthorized responses by silently rotating the access token and retrying the request once.
- Signs the user out only when the session is unrecoverable (refresh fails or the retried request is also rejected).
- Unwraps the response data.
- Provides typed error handling via
ApiError.
1import { executeRest } from '@/api/common';23// Basic GET request4const data = await executeRest<User>('/users/me', 'GET');56// POST request with body7await executeRest('/auth/login', 'POST', {8 email: 'user@example.com',9 password: 'password'10});1112// Request with custom options (headers, params)13await executeRest('/search', 'GET', undefined, {14 params: { q: 'react' },15 ignore401: true // Skip refresh + sign-out for this call16});
Refresh-Token Rotation
When a request comes back 401, the client posts the stored refresh token to EXPO_PUBLIC_REFRESH_URL (see src/api/common/refresh-auth.ts), persists the rotated token pair via the auth store, and retries the original request once with the fresh access token.
- Single-flight: concurrent 401s share one in-flight refresh request, so parallel queries don't hammer the refresh endpoint.
- Graceful failure: a malformed response, missing session, or network error returns without throwing — the request simply fails with a typed
ApiError. - Sign-out: only when the refresh fails or the rotated token is itself rejected, the session is cleared and the query cache reset.
- Set
ignore401: trueon a request to skip both refresh and sign-out for that call.
The refresh endpoint is backend-configurable per environment via EXPO_PUBLIC_REFRESH_URL (configured in each .env.* file and validated in root-env.js).
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";45// 1. Create the base query6const _useGetUser = createQuery<TUser, void, AxiosError>({7 queryKey: [QueryKey.USER], // unique key for caching8 fetcher: async () => {9 const response = await executeRest<{ data: TUser }>("auth/me", "GET");10 return response?.data;11 },12});1314// 2. Export a custom hook wrapper for better DX15export const useGetUser = (userId?: string) => {16 const { auth_data } = useAuth();1718 // Only run query if user is logged in19 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";34const 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});1314// Usage in component15const { 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';23export 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 10/4/2026
Edit this page on GitHub