This episode covers app-to-server communication: fetch and Axios with error handling, base URLs per environment, caching and optimistic updates with TanStack Query, plus an introduction to state management with Zustand.

Almost every mobile app needs data from a server: product lists, user profiles, or news feeds. In episode 6 you learned to move between screens; now it's time to connect those screens to the outside world via APIs.
Episode 7 covers the right data fetching patterns in React Native: the built-in fetch and Axios for HTTP requests with error handling, base URLs per environment, then TanStack Query for caching and optimistic updates, plus Zustand as global state management. You'll keep using these patterns in episodes 9, 13, and 15.
React Native provides the same fetch as the web. For simple calls, write it with async/await and always handle errors:
async function ambilPostingan() {
try {
const res = await fetch(`${API_URL}/posts`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return await res.json();
} catch (error) {
console.error("Gagal mengambil data", error);
return [];
}
}Note two important things: check res.ok before reading the body, and wrap everything in try/catch. Mobile networks often fail — weak signals, timeouts, and connections lost when the app moves to the background.
For production needs like interceptors, timeouts, and request cancellation, Axios is more convenient. Install it first:
npm install axiosCreate one instance used across the whole app:
import axios from "axios";
const api = axios.create({
baseURL: API_URL,
timeout: 15000,
headers: { "Content-Type": "application/json" },
});
api.interceptors.request.use((config) => {
const token = getToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});With axios.create({ baseURL, timeout }), all requests use the same base URL and timeout. The interceptor adds the Authorization header automatically — just one place for all requests.
The server URL must not be hardcoded in the code. Use the react-native-config library so the values are read from a .env file:
npm install react-native-configThe .env file is placed at the project root and contains key-value pairs like API_URL=https://api.example.com and ENV=development.
react-native-config maps .env values to a global object at build time:
import Config from "react-native-config";
export const API_URL = Config.API_URL;
export const IS_PRODUCTION = Config.ENV === "production";With this pattern you can have .env.staging and .env.production, then choose which file is used at build time. Remember: never commit secrets from .env — episode 13 will discuss the security in more depth.
Manual fetch requires you to write loading, error, and caching yourself. TanStack Query automates all of it:
npm install @tanstack/react-queryimport { useQuery } from "@tanstack/react-query";
function usePosts() {
return useQuery({
queryKey: ["posts"],
queryFn: ambilPostingan,
staleTime: 5 * 60 * 1000,
});
}queryKey: ["posts"] identifies the data in the cache, and staleTime controls how long the data is considered fresh. Components just read isLoading, isError, and data from the hook result.
For actions like adding a post, update the UI first, then sync to the server. If it fails, restore the old state:
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: simpanPostingan,
onMutate: async (baru) => {
await queryClient.cancelQueries({ queryKey: ["posts"] });
const sebelumnya = queryClient.getQueryData(["posts"]);
queryClient.setQueryData(["posts"], (lama) => [baru, ...lama]);
return { sebelumnya };
},
onError: (_err, _baru, konteks) => {
queryClient.setQueryData(["posts"], konteks.sebelumnya);
},
});The onMutate pattern writes new data to the cache before the server responds, so the UI feels instant. onError restores the old data from the context returned by onMutate.
For data shared across screens like tokens and profiles, Zustand provides a global store with a very small API:
npm install zustandimport { create } from "zustand";
export const useAuthStore = create((set) => ({
token: null,
user: null,
setLogin: (token, user) => set({ token, user }),
logout: () => set({ token: null, user: null }),
}));Components use the useAuthStore hook directly, for example useAuthStore((state) => state.token) to read the token. Selecting only the fields you need makes components re-render only when the token changes.
Redux is still relevant for large apps with many developers, but for the majority of cases Zustand is much lighter.
Tip
Don't store server data in global state. Data from APIs is TanStack Query's business; Zustand is enough for truly global client data like tokens and preferences. Mixing the two makes the cache and state collide with each other.
Episode 7 connected the app to the server: fetch and Axios with proper error handling, base URLs per environment via react-native-config, TanStack Query for caching and optimistic updates, and Zustand for lightweight global state.
Key takeaways:
fetch and Axios are both valid; Axios excels at interceptors and timeouts.res.ok and handle mobile network errors..env per environment, not hardcoded.onError.In the next episode, episode 8, we'll discuss lists, scroll, and basic performance: FlatList and SectionList with keyExtractor and getItemLayout, windowing, onEndReached for infinite scroll, and render optimization with memo and stable keys.