React Native State Management with Redux Toolkit: A Practical Guide

by Didin J. on Aug 29, 2025 React Native State Management with Redux Toolkit: A Practical Guide

Learn React Native state management with Redux Toolkit. Step-by-step guide with Expo Router, slices, store setup, DevTools, and best practices.

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 a counter 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)

  1. Download and install React Native Debugger.

  2. Open it on the port 19000 (or the port Expo Dev Tools uses).

  3. 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 and useDispatch (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:

Thanks!