Managing Server State and UI State in React Native

Managing Server State and UI State in React Native
As a React Native application grows, managing data becomes more complicated.
You may need to fetch user profiles from an API, display product lists, manage form inputs, open modals, track selected tabs, and keep data available when users return to a screen.
A common mistake is treating all this information as the same kind of state.
But server data and interface data have different responsibilities. Understanding the difference helps you choose the right state management tools and build applications that are easier to maintain.
In this article, we will explore server state, UI state, Redux Toolkit, TanStack Query, and practical patterns for managing state in React Native applications.
1. What Is State in React Native?
State is information that an application remembers and uses to determine what should be displayed or how it should behave.
For example, consider a shopping application.
The application might need to remember:
- The products retrieved from an API.
- Whether the product request is loading.
- The currently selected product category.
- Whether a filter modal is open.
- The user's shopping cart.
- The current search query.
All of these are state, but they do not necessarily belong in the same place.
The first step is to understand where the information comes from and who is responsible for keeping it up to date.
2. What Is Server State?
Server state is data that originates from an external source, usually a backend API or database.
Examples include:
- User profiles.
- Product listings.
- Order history.
- Notifications retrieved from a server.
- Course information.
- Account settings stored in a backend.
Consider this request:
const response = await fetch(
"https://api.example.com/products"
);
const products = await response.json();The product data comes from the server. Your mobile application receives a copy of that data, but the backend remains responsible for its authoritative version.
This introduces several challenges:
- The network may be slow or unavailable.
- The data may become outdated.
- Multiple screens may need the same data.
- Requests may fail.
- Data may need to be refreshed.
- Cached responses may need invalidation.
Server state management tools help handle these challenges.
3. What Is UI State?
UI state, sometimes called client state, represents information that controls the local interface.
Examples include:
- Whether a modal is open.
- Which tab is selected.
- Whether a password is visible.
- The current text inside a search field.
- Whether an accordion is expanded.
- The currently selected item in a local picker.
For example:
const [isModalVisible, setIsModalVisible] =
useState(false);This state belongs to the interface. It does not need to be fetched from a backend.
When a user taps a button, the application can update the state:
setIsModalVisible(true);React Native then renders the interface according to the updated state.
For simple local interactions, React's built-in useState hook is often enough.
4. Server State vs. UI State
| Aspect | Server State | UI State |
|---|---|---|
| Source | Backend or external service | User interaction or local application logic |
| Examples | Products, profiles, orders | Modal visibility, selected tab |
| Main challenges | Network errors, stale data, caching | Local updates and component behavior |
| Typical tools | TanStack Query, RTK Query | useState, useReducer, Redux Toolkit |
| Refresh behavior | May require refetching or invalidation | Usually updated by local actions |
| Sharing | Often shared across screens | Local or globally shared when necessary |
The key distinction is ownership.
The server owns the authoritative product information. The mobile app owns the current state of a locally opened modal.
Some data can involve both sides. A shopping cart, for example, might exist locally, on a server, or in both places. Its state management depends on synchronization and persistence requirements.
5. Why One State Management Tool Is Not Always Enough
Many React Native developers begin by placing everything inside Redux.
This can work, but it may create unnecessary complexity when server requests, loading indicators, errors, and cache invalidation are all implemented manually.
Consider a product screen. You need to manage:
- The product response.
- Loading status.
- Request errors.
- Cached results.
- Refetching after updates.
- Modal visibility.
- Search input.
These concerns do not all need the same solution.
A more maintainable approach is to select tools based on the type of state being managed.
For example:
useStatefor local interface interactions.- TanStack Query or RTK Query for API data.
- Redux Toolkit for shared client state and application-wide workflows when needed.
These tools can work together. You do not have to choose only one.
6. Manage Local UI State with useState
Suppose you are building a product screen with a filter modal.
import { useState } from "react";
import {
Button,
Modal,
Text,
View,
} from "react-native";
export default function ProductFilters() {
const [visible, setVisible] = useState(false);
return (
<View>
<Button
title="Open filters"
onPress={() => setVisible(true)}
/>
<Modal
visible={visible}
transparent
animationType="slide"
onRequestClose={() => setVisible(false)}
>
<View>
<Text>Product Filters</Text>
<Button
title="Close"
onPress={() => setVisible(false)}
/>
</View>
</Modal>
</View>
);
}The modal visibility is local UI state.
There is no need to store it in a global Redux store unless multiple parts of the application genuinely need to control or observe that state.
Keeping state close to the component that uses it reduces unnecessary coupling.
7. Manage Server State with TanStack Query
TanStack Query is a library for fetching, caching, synchronizing, and updating server data.
It can manage:
- Loading and error states.
- Cached API responses.
- Background refetching.
- Query invalidation.
- Request deduplication for matching queries.
- Data freshness policies.
Install it:
npm install @tanstack/react-queryCreate a query client and provide it to your application:
// App.tsx
import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query";
import ProductScreen from "./src/screens/ProductScreen";
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<ProductScreen />
</QueryClientProvider>
);
}In a larger application, create the client in a dedicated module or stable application-level location so it is not recreated on every render.
Now create a function that fetches products:
// src/api/products.ts
export type Product = {
id: string;
name: string;
price: number;
};
export async function fetchProducts(): Promise<Product[]> {
const response = await fetch(
"https://api.example.com/products"
);
if (!response.ok) {
throw new Error("Failed to fetch products");
}
return response.json();
}Replace the example URL with your actual API endpoint.
Use the function in a screen:
// src/screens/ProductScreen.tsx
import { useQuery } from "@tanstack/react-query";
import {
ActivityIndicator,
FlatList,
Text,
View,
} from "react-native";
import { fetchProducts } from "../api/products";
export default function ProductScreen() {
const {
data,
isPending,
isError,
error,
} = useQuery({
queryKey: ["products"],
queryFn: fetchProducts,
});
if (isPending) {
return <ActivityIndicator />;
}
if (isError) {
return (
<Text>
{error.message || "Something went wrong"}
</Text>
);
}
return (
<FlatList
data={data}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View>
<Text>{item.name}</Text>
<Text>{item.price}</Text>
</View>
)}
/>
);
}TanStack Query manages the server-data lifecycle instead of requiring you to manually maintain separate loading, error, and cached-data variables.
For production applications, also consider empty results, retry behavior, offline scenarios, and user-friendly error messages.
8. Understand Query Keys and Caching
A query key identifies a particular piece of server data in TanStack Query.
For example:
useQuery({
queryKey: ["products"],
queryFn: fetchProducts,
});The key ["products"] identifies the product list.
If you fetch an individual product, include its ID:
useQuery({
queryKey: ["products", productId],
queryFn: () => fetchProductById(productId),
});Different product IDs represent different queries.
If a request depends on filters, pagination, or sorting, include those values in the query key too:
useQuery({
queryKey: ["products", category, page],
queryFn: () => fetchProductsByCategory(category, page),
});A correct query key prevents unrelated requests from sharing the same cached entry.
TanStack Query can reuse cached data and decide when it should be considered stale. Stale data does not necessarily disappear immediately; it can remain available while a refetch occurs, depending on the configuration and query lifecycle.
Caching API responses is not the same as permanently saving data to device storage. Persistence across application restarts requires additional configuration.
9. What Is RTK Query?
If your application already uses Redux Toolkit, RTK Query is another strong option for managing server state.
RTK Query provides API fetching, caching, request status tracking, and cache invalidation through Redux Toolkit.
Install Redux Toolkit and React Redux if they are not already installed:
npm install @reduxjs/toolkit react-reduxDefine an API service:
// src/services/productApi.ts
import {
createApi,
fetchBaseQuery,
} from "@reduxjs/toolkit/query/react";
export const productApi = createApi({
reducerPath: "productApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://api.example.com/",
}),
endpoints: (builder) => ({
getProducts: builder.query({
query: () => "products",
}),
}),
});
export const { useGetProductsQuery } =
productApi;Register the API reducer and middleware in the Redux store:
// src/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import { productApi } from "../services/productApi";
export const store = configureStore({
reducer: {
[productApi.reducerPath]: productApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(productApi.middleware),
});Wrap your application in the Redux provider:
import { Provider } from "react-redux";
import { store } from "./src/store";
import ProductScreen from "./src/screens/ProductScreen";
export default function App() {
return (
<Provider store={store}>
<ProductScreen />
</Provider>
);
}You can now use the generated query hook:
const {
data,
isLoading,
isError,
} = useGetProductsQuery();This example illustrates the basic integration. For a production application, define explicit response types for API endpoints and add tags or other invalidation rules where necessary.
10. TanStack Query vs. RTK Query
Both libraries are capable tools for managing server state, but they fit different application preferences.
| Feature | TanStack Query | RTK Query |
|---|---|---|
| Main role | Server-state management | Server-state management integrated with Redux Toolkit |
| Global Redux store required | No | Yes, for its normal Redux integration |
| API definition | Query functions and query options | Centralized API service definitions |
| Cache invalidation | Query keys and invalidation APIs | Tags and invalidation rules |
| Best fit | Apps wanting dedicated server-state tools | Apps already using Redux Toolkit |
Neither library is universally better.
If your application already uses Redux Toolkit extensively, RTK Query can reduce the need for a separate server-state library.
If you want server-state management without making Redux a requirement for the application, TanStack Query is a useful option.
Avoid adding both libraries for the same API data unless you have a clear reason. Maintaining duplicate caches can introduce unnecessary complexity.
11. Where Does Redux Toolkit Fit?
Redux Toolkit is useful for shared client-side state that multiple screens or components need to access.
Examples include:
- A locally managed shopping cart.
- Application-wide preferences.
- Multi-step workflows.
- Client-side selections shared across screens.
- State that requires predictable, centralized updates.
For example, a cart slice might look like this:
// src/features/cart/cartSlice.ts
import {
createSlice,
PayloadAction,
} from "@reduxjs/toolkit";
type CartItem = {
productId: string;
quantity: number;
};
type CartState = {
items: CartItem[];
};
const initialState: CartState = {
items: [],
};
const cartSlice = createSlice({
name: "cart",
initialState,
reducers: {
addToCart(
state,
action: PayloadAction<CartItem>
) {
const existingItem = state.items.find(
(item) =>
item.productId === action.payload.productId
);
if (existingItem) {
existingItem.quantity += action.payload.quantity;
} else {
state.items.push(action.payload);
}
},
clearCart(state) {
state.items = [];
},
},
});
export const { addToCart, clearCart } =
cartSlice.actions;
export default cartSlice.reducer;This example represents a locally managed cart. A real shopping application must also decide how the cart is synchronized with the backend, persisted between sessions, and reconciled across devices.
Redux Toolkit uses Immer internally, allowing this reducer syntax while preserving immutable state updates.
12. A Practical State Management Architecture
A React Native application can separate its state by responsibility.
React Native Application
|
+-- Local Component State
| +-- Modal visibility
| +-- Form inputs
| +-- Selected tab
|
+-- Shared Client State
| +-- Cart selections
| +-- Local preferences
| +-- Multi-screen workflows
|
+-- Server State
+-- User profile
+-- Products
+-- Orders
+-- NotificationsPossible tools for each layer:
Local Component State
|
v
useState
Shared Client State
|
v
Redux Toolkit
Server State
|
+----> TanStack Query
|
+----> RTK QueryThis is a guide rather than a strict rule. For example, context and useReducer can handle some shared state without Redux, and a small application may need only React hooks plus one API-fetching approach.
Choose the smallest set of tools that handles your application's requirements well.
13. Common State Management Mistakes
Mistake 1: Putting everything into Redux
Not every temporary UI interaction needs to live in a global store.
Keep local state local unless other parts of the application need it.
Mistake 2: Fetching the same data independently on every screen
If multiple screens use the same server data, a query cache can help reuse responses and coordinate refetching.
Mistake 3: Treating cached data as permanently fresh
Server data changes. Configure freshness, refetching, and invalidation according to the needs of the application.
Mistake 4: Duplicating the same server data in multiple stores
Storing the same product list in a query cache and a Redux slice can create synchronization problems.
Keep one clear source for each kind of state unless duplication is deliberate and justified.
Mistake 5: Ignoring loading, empty, and error states
A screen should handle more than a successful response. Plan for loading indicators, empty results, network errors, and retry options.
Mistake 6: Assuming in-memory state survives an app restart
React state, Redux state, and ordinary query caches are generally held in memory. Persistence requires a separate strategy, such as suitable device storage and a hydration process.
Never store sensitive information in an insecure location merely to make state persistent.
14. A Practical Checklist
Before implementing state management in a React Native feature, ask:
- Does the data come from the server or from local interactions?
- Can the state remain inside a component?
- Do multiple screens need access to it?
- Is the data duplicated across different stores?
- How will loading and error states be displayed?
- How will stale data be refreshed or invalidated?
- Does the data need to survive application restarts?
- Are sensitive values stored securely?
- Is the chosen library solving a real problem?
Conclusion
Managing state in React Native becomes easier when you distinguish between data owned by the server and state owned by the client interface.
Use React's built-in hooks for local UI interactions. Use a dedicated server-state library such as TanStack Query or RTK Query when API fetching, caching, and invalidation become important. Use Redux Toolkit when shared client-side state benefits from centralized, predictable updates.
You do not need to introduce every state management library into every project. Start with the simplest approach that fits your requirements and add complexity only when it provides a clear benefit.
Remember: Choose state management tools based on who owns the data and how it changes, not simply because a library is popular. w


