State management is one of the most important aspects of building scalable React Native applications. As your app grows, passing data between multiple components using props can quickly become messy and difficult to maintain. This is where a centralized state management solution like Redux Toolkit comes in.
Redux Toolkit (RTK) is the official, recommended way to use Redux. It simplifies common Redux tasks such as store setup, reducers, and immutable state updates by providing a clean and modern API. With RTK, you can focus more on your app’s logic instead of boilerplate code.
In this tutorial, you’ll learn how to integrate Redux Toolkit into a React Native project. We’ll start with a simple counter example and then move on to a more practical case to show how Redux Toolkit can help manage complex app state efficiently.
By the end, you will be able to:
-
Set up Redux Toolkit in a React Native app.
-
Create and manage slices of state.
-
Use hooks (
useSelector
,useDispatch
) to connect your components to the store. -
Apply best practices to keep your state predictable and maintainable.
Step 1: Create a New React Native Project
For this tutorial, we’ll use Expo because it makes starting a React Native project quick and easy. Expo comes with a lot of useful tools, and you don’t need to worry about native build configurations in the beginning.
Open your terminal and run the following command to create a new project:
npx create-expo-app redux-toolkit-demo
cd redux-toolkit-demo
Once the installation is complete, you can start the development server with:
npm start
This will launch Expo Dev Tools in your browser. From there, you can run the app on an emulator or a physical device using the Expo Go app.
At this point, you should see the default Expo welcome screen, which means your React Native project is up and running. 🎉
Step 2: Install Redux Toolkit and React-Redux
Now that the React Native project is ready, the next step is to install Redux Toolkit and React-Redux.
Redux Toolkit (@reduxjs/toolkit
) provides utilities to simplify Redux setup, while React-Redux is the official binding that allows your React Native components to interact with the Redux store.
Run the following command inside your project folder:
npm install @reduxjs/toolkit react-redux
Or if you prefer Yarn:
yarn add @reduxjs/toolkit react-redux
Once installed, you’re ready to create your first slice, which represents a piece of the application state along with the logic to modify it. We’ll set this up in the next step.
Step 3: Create a Redux Slice
In Redux Toolkit, a slice represents a single piece of your app’s state, along with the reducers (functions) that update that state. Each slice automatically generates action creators and action types, which saves a lot of boilerplate code compared to traditional Redux.
Let’s create a simple counter slice as our first example.
1. Create the slice file
Inside your project, create a new folder structure like this:
redux-toolkit-demo/
└── features/
└── counter/
└── counterSlice.ts
2. Add counter slice code
Open counterSlice.ts
and add the following code:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
reset: (state) => {
state.value = 0;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, reset, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
How this works:
-
createSlice
takes a name, initial state, and reducer functions. -
Each reducer modifies the state directly (thanks to Immer under the hood).
-
Redux Toolkit automatically generates actions (
increment
,decrement
, etc.) for each reducer. -
We export both the actions and the reducer.
Next, we’ll set up the Redux store and add this slice to it.
Step 4: Configure the Store
The store is the central place where your app’s state lives. With Redux Toolkit, we can easily configure it using the configureStore
function.
1. Create a store file
Inside the root
folder, create a new folder called app
and add a file named store.ts
:
redux-toolkit-demo/
└── app/
└── store.ts
2. Add the store configuration
Open store.js
and add the following code:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Here’s what’s happening:
-
We import the
counterReducer
from the slice we created. -
configureStore
sets up the store with acounter
reducer. -
You can add multiple slices in the
reducer
object as your app grows.
3. Provide the store to the app
To make Redux available throughout the app, wrap the root component with Provider
from react-redux
.
Open your app/(tabs)/_layout.tsx
(inside the project root) and update it like this:
import { Tabs } from "expo-router";
import React from "react";
import { Platform } from "react-native";
import { HapticTab } from "@/components/HapticTab";
import { IconSymbol } from "@/components/ui/IconSymbol";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { Colors } from "@/constants/Colors";
import { useColorScheme } from "@/hooks/useColorScheme";
// Update the import path below to the correct relative path if "@/store" does not exist
import { Provider } from "react-redux";
import { store } from "../store";
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
<Provider store={store}>
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: "absolute"
},
default: {}
})
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="house.fill" color={color} />
)
}}
/>
<Tabs.Screen
name="explore"
options={{
title: "Explore",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="paperplane.fill" color={color} />
)
}}
/>
</Tabs>
</Provider>
);
}
Now your app has a Redux store available to all components. 🎉
Step 5: Use Redux State in Components
With Redux Toolkit set up, we can now interact with the store inside our screens. This involves two main hooks:
-
useSelector
→ to read values from the Redux store -
useDispatch
→ to dispatch actions (update state)
We’ll continue with the counter slice we created earlier.
1. Reading State with useSelector
The useSelector
hook lets you access any slice of state from the store.
Example in app/(tabs)/index.tsx
:
import { Text, View } from "react-native";
import { useSelector } from "react-redux";
import { RootState } from "../store";
export default function HomeScreen() {
const count = useSelector((state: RootState) => state.counter.value);
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={{ fontSize: 24 }}>Count: {count}</Text>
</View>
);
}
Here, we’re pulling the value
from our counter
slice.
2. Updating State with useDispatch
To change state, we use the useDispatch
hook, along with the actions we exported from counterSlice.ts
.
Update app/(tabs)/index.tsx
:
import { Button, Text, View } from "react-native";
import {
decrement,
increment,
incrementByAmount,
reset
} from "@/features/counter/counterSlice";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../store";
export default function HomeScreen() {
const count = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: 10
}}
>
<Text style={{ fontSize: 24, marginBottom: 20 }}>Count: {count}</Text>
<Button title="Increment" onPress={() => dispatch(increment())} />
<Button title="Decrement" onPress={() => dispatch(decrement())} />
<Button title="Reset" onPress={() => dispatch(reset())} />
<Button
title="Increment by 5"
onPress={() => dispatch(incrementByAmount(5))}
/>
</View>
);
}
Now you can:
-
Increase the counter
-
Decrease the counter
-
Reset it
-
Increase by a specific number (
incrementByAmount
)
✅ At this stage, you have a fully functional counter app with Redux Toolkit in Expo Router.
Step 6: Create a More Realistic Example (Todo List)
We’ll extend our app by managing a list of todos (add, toggle complete, delete) using Redux Toolkit.
1. Create the Todo Slice
Inside features/
, create a new folder todos
and a file todoSlice.ts
:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface Todo {
id: string;
text: string;
completed: boolean;
}
interface TodoState {
items: Todo[];
}
const initialState: TodoState = {
items: [],
};
const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action: PayloadAction<string>) => {
state.items.push({
id: Date.now().toString(),
text: action.payload,
completed: false,
});
},
toggleTodo: (state, action: PayloadAction<string>) => {
const todo = state.items.find((t) => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action: PayloadAction<string>) => {
state.items = state.items.filter((t) => t.id !== action.payload);
},
},
});
export const { addTodo, toggleTodo, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
2. Register Todo Slice in the Store
Update app/store.ts
:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';
import todoReducer from '../features/todoSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todoReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
3. Create a Todo Screen
Inside app/(tabs)/
, add a new file todos.tsx
:
import React, { useState } from "react";
import {
Button,
FlatList,
Text,
TextInput,
TouchableOpacity,
View
} from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../app/store";
import { addTodo, deleteTodo, toggleTodo } from "../../features/todoSlice";
export default function TodosScreen() {
const [text, setText] = useState("");
const todos = useSelector((state: RootState) => state.todos.items);
const dispatch = useDispatch();
return (
<View style={{ flex: 1, padding: 20 }}>
<Text style={{ fontSize: 28, marginBottom: 10 }}>Todo List</Text>
{/* Input for new todo */}
<View style={{ flexDirection: "row", marginBottom: 20 }}>
<TextInput
value={text}
onChangeText={setText}
placeholder="Enter a todo"
style={{
flex: 1,
borderWidth: 1,
borderColor: "#ccc",
padding: 10,
marginRight: 10,
borderRadius: 5
}}
/>
<Button
title="Add"
onPress={() => {
if (text.trim()) {
dispatch(addTodo(text));
setText("");
}
}}
/>
</View>
{/* Todo list */}
<FlatList
data={todos}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: 10
}}
>
<TouchableOpacity
style={{ flex: 1 }}
onPress={() => dispatch(toggleTodo(item.id))}
>
<Text
style={{
fontSize: 18,
textDecorationLine: item.completed ? "line-through" : "none"
}}
>
{item.text}
</Text>
</TouchableOpacity>
<Button
title="Delete"
onPress={() => dispatch(deleteTodo(item.id))}
/>
</View>
)}
/>
</View>
);
}
4. Add Todo Tab
If you’re using Expo Router Tabs, open app/(tabs)/_layout.tsx
(or wherever your tab layout is defined) and add the new screen:
import { Tabs } from "expo-router";
import React from "react";
import { Platform } from "react-native";
import { HapticTab } from "@/components/HapticTab";
import { IconSymbol } from "@/components/ui/IconSymbol";
import TabBarBackground from "@/components/ui/TabBarBackground";
import { Colors } from "@/constants/Colors";
import { useColorScheme } from "@/hooks/useColorScheme";
// Update the import path below to the correct relative path if "@/store" does not exist
import { Provider } from "react-redux";
import { store } from "../store";
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
<Provider store={store}>
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: "absolute"
},
default: {}
})
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="house.fill" color={color} />
)
}}
/>
<Tabs.Screen
name="explore"
options={{
title: "Explore",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="paperplane.fill" color={color} />
)
}}
/>
<Tabs.Screen name="todos" options={{ title: "Todos" }} />
</Tabs>
</Provider>
);
}
✅ Now you have a fully working Todo List with Redux Toolkit in Expo Router:
-
Add new todos
-
Toggle completed
-
Delete todos
Step 7: Debugging and Redux DevTools
One of the biggest advantages of Redux is its excellent debugging ecosystem. With Redux DevTools, you can:
-
Inspect your app’s state in real-time
-
View a timeline of dispatched actions
-
Travel back and forth in state history
Even better: Redux Toolkit already has Redux DevTools enabled by default, so you don’t need extra setup.
1. Redux DevTools Setup
-
If you’re running your app in Expo Go, DevTools won’t connect directly. Instead, you can use:
-
React Native Debugger (recommended)
-
Or Flipper (Meta’s debugging tool with Redux integration)
-
Using React Native Debugger (most popular choice)
-
Download and install React Native Debugger.
-
Open it on the port
19000
(or the port Expo Dev Tools uses). -
In your app, enable “Debug Remote JS” from the developer menu.
Your Redux store should automatically connect to Redux DevTools inside React Native Debugger.
2. Verify DevTools Connection
When you interact with your app (e.g., add or toggle a todo), you should see:
-
Actions (like
todos/addTodo
,todos/toggleTodo
) appear in the left panel. -
State updates in the right panel.
This makes it easy to:
-
Track bugs
-
Replay or undo actions
-
Confirm reducers behave as expected
3. Example in Action
Say you press “Add” in the Todo screen.
-
Redux dispatches
todos/addTodo
with your todo text. -
Redux DevTools will show the action details and the updated state (
items
array).
If you press “Toggle”, you’ll see todos/toggleTodo
and the completed
field flip between true/false
.
✅ With DevTools set up, debugging your Redux Toolkit state becomes much more transparent and developer-friendly.
Step 8: Best Practices for Redux Toolkit in React Native
1. Use Typed Hooks (useAppDispatch
, useAppSelector
)
Instead of importing useDispatch
and useSelector
everywhere, create typed hooks so TypeScript can infer types automatically.
👉 Create a new file: app/hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Typed versions of useDispatch and useSelector
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Now you can replace them in your components:
// Before
const dispatch = useDispatch();
const count = useSelector((state: RootState) => state.counter.value);
// After
const dispatch = useAppDispatch();
const count = useAppSelector((state) => state.counter.value);
This avoids repetitive type annotations. ✅
2. Keep Slices Small and Focused
-
Each slice should manage one concern (e.g.,
counter
,todos
,auth
). -
Don’t dump your entire app’s state into one giant slice.
-
Organize by feature:
src/features/<featureName>/<sliceName>.ts
.
3. Use Immer Wisely
Redux Toolkit uses Immer under the hood, so you can “mutate” state safely:
increment: (state) => {
state.value += 1; // looks mutable, but is actually immutable
}
⚠️ Still, avoid very deeply nested objects — keep state flat when possible for performance and maintainability.
4. Async Logic → Use RTK Query or Thunks
For API calls:
-
Prefer RTK Query (built into Redux Toolkit) for data fetching and caching.
-
Use createAsyncThunk for simple async actions if RTK Query feels overkill.
Example with RTK Query:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),
endpoints: (builder) => ({
getTodos: builder.query<any[], void>({
query: () => 'todos',
}),
}),
});
export const { useGetTodosQuery } = api;
Then add api.reducer
to the store, and use the hook:
const { data, error, isLoading } = useGetTodosQuery();
5. Persist State for Better UX
For real-world apps (like auth, shopping carts), persist Redux state with redux-persist:
npm install redux-persist
This ensures the state is saved between app restarts.
✅ Following these practices will keep your Redux Toolkit + React Native app scalable, clean, and production-ready.
Conclusion
In this tutorial, you learned how to set up Redux Toolkit in a React Native project using the latest Expo Router structure. We started with a simple counter example to understand the basics of slices, reducers, and actions, then built a more practical Todo List app to show how Redux Toolkit can handle real-world state management.
Here’s what we covered:
-
Setting up Redux Toolkit and integrating it with Expo Router.
-
Creating slices (
counterSlice
,todoSlice
) to manage different pieces of state. -
Using
useSelector
anduseDispatch
(and later typed hooks) to connect components to the store. -
Debugging with Redux DevTools for better visibility and troubleshooting.
-
Applying best practices such as organizing slices by feature, keeping state flat, using typed hooks, and considering RTK Query or redux-persist for advanced use cases.
Redux Toolkit eliminates much of the boilerplate that made Redux intimidating in the past, making it easier than ever to manage global state in your React Native apps. With these tools and patterns, you’ll be able to scale your applications confidently and keep your state predictable and maintainable.
Now that you’ve mastered the basics, here are some next steps to explore:
-
Use RTK Query for data fetching and caching.
-
Add redux-persist to keep state between app restarts.
-
Explore more complex state management patterns (e.g., auth, user sessions, offline sync).
By combining Redux Toolkit with Expo and React Native, you’re well on your way to building scalable, real-world mobile apps with robust state management. 🚀
You can get the full source code on our GitHub.
That's just the basics. If you need more deep learning about React.js, React Native, or related, you can take the following cheap course:
- React JS with Redux Certification Training
- React Projects Course: Build Real World Projects
- React Testing with Jest / Vitest - TypeScript - 2025
- React Supabase CRUD App
- React JS & Firebase Complete Course (incl. Chat Application)
- Build the original Instagram with React Native & Firebase
- Master React Native Animations
- Learn How To Create React Native Application & WordPress Api
- Build React Native Apps for Android and iOS
- The Complete ChatGPT with React Native - Mobile Application
Thanks!