Setting up Redux Architecture for React Native Applications
Redux in React Native is a classic. Before hooks and modern alternatives, it was the only reliable way to manage complex state. Now Redux Toolkit (RTK) removed the main argument against it: boilerplate. With createSlice, createAsyncThunk, and RTK Query, code is twice as compact, and TypeScript type safety is complete.
Redux Toolkit: Modern Redux
// store/slices/profileSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { userApi } from '../api/userApi';
export const fetchProfile = createAsyncThunk(
'profile/fetch',
async (userId: string, { rejectWithValue }) => {
try {
return await userApi.getProfile(userId);
} catch (e) {
return rejectWithValue((e as Error).message);
}
}
);
interface ProfileState {
data: UserProfile | null;
loading: boolean;
error: string | null;
}
const profileSlice = createSlice({
name: 'profile',
initialState: { data: null, loading: false, error: null } as ProfileState,
reducers: {
clearProfile: (state) => { state.data = null; },
},
extraReducers: (builder) => {
builder
.addCase(fetchProfile.pending, (state) => { state.loading = true; state.error = null; })
.addCase(fetchProfile.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchProfile.rejected, (state, action) => {
state.loading = false;
state.error = action.payload as string;
});
},
});
Immer is built into RTK: mutations inside createSlice are safe, immutability is guaranteed under the hood.
RTK Query for Server State
For API requests with caching, RTK Query is the best choice in the RTK ecosystem:
export const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getProfile: builder.query<UserProfile, string>({
query: (userId) => `/users/${userId}`,
providesTags: (result, error, id) => [{ type: 'User', id }],
}),
updateProfile: builder.mutation<UserProfile, Partial<UserProfile>>({
query: (body) => ({ url: `/users/${body.id}`, method: 'PUT', body }),
invalidatesTags: (result, error, arg) => [{ type: 'User', id: arg.id }],
}),
}),
});
export const { useGetProfileQuery, useUpdateProfileMutation } = userApi;
In component: const { data, isLoading, error } = useGetProfileQuery(userId). Caching, request deduplication, invalidation — all out of the box.
Middleware and Redux Saga
For complex async scenarios (request chains, WebSocket, request cancellation) createAsyncThunk may be insufficient. Redux-Saga provides a generator-based approach:
function* fetchProfileSaga(action: ReturnType<typeof fetchProfile>) {
try {
const profile: UserProfile = yield call(userApi.getProfile, action.payload);
yield put(profileLoaded(profile));
} catch (e) {
yield put(profileFailed((e as Error).message));
}
}
Sagas are handy for: parallel requests (all), cancellation (race + cancel), retries (retry). For most projects createAsyncThunk suffices.
Store Typing
export const store = configureStore({
reducer: {
profile: profileSlice.reducer,
[userApi.reducerPath]: userApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(userApi.middleware),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Typed hooks
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
What We Configure
Setup of configureStore with RTK. Slice structure by features. RTK Query for API layer. Typed hooks. redux-persist setup for persistence (if needed). Basic slice tests via Jest.
Timeline
Setup of RTK + RTK Query from scratch: 2–3 days. Migration from legacy Redux (actions/reducers/thunks) to RTK: 1–2 weeks. Cost — individually.







