DEV Community

Cover image for React Native + Redux Toolkit: The Clean Way to Handle State
Amit Kumar
Amit Kumar

Posted on

React Native + Redux Toolkit: The Clean Way to Handle State

Managing state in React Native can get tricky as apps grow. Counters, inputs, API calls — they all add complexity.

That’s where Redux Toolkit (RTK) shines. It removes boilerplate, enforces best practices, and keeps your app state predictable.

In this post, we’ll build a React Native app with Redux Toolkit that covers three common use cases:

  • A counter state
  • An input field state
  • Fetching API data

🗂 Project Setup

Here’s the folder structure we’ll be working with:

src/
  screens/
   HomeScreen/
    index.js
   ProfileScreen/
  store/
   slice/
    apiSlice.js
    counterSlice.js
    InputSlice.js
   store.js
  vendor/

Enter fullscreen mode Exit fullscreen mode

This structure clearly separates UI (screens) and state management (store). Each feature gets its own slice inside store/slice/, keeping logic modular and easy to maintain.


⚡ Configuring Redux Store

In store.js, we configure Redux Toolkit with three slices:

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './slice/counterSlice';
import inputReducer from './slice/InputSlice';
import apiReducer from './slice/apiSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    input: inputReducer,
    api: apiReducer,
  },
});


Enter fullscreen mode Exit fullscreen mode

Now, Redux knows how to handle counter, input, and API states independently.


Counter Slice

Let’s start with a simple counter.


import { createSlice } from '@reduxjs/toolkit';

const initialState = { counterValue: 0 };

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => { state.counterValue += 1; },
    decrement: state => { state.counterValue -= 1; },
    reset: state => { state.counterValue = 0; },
  },
});

export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;


Enter fullscreen mode Exit fullscreen mode

With just a few lines, we now have three actions: increment, decrement, and reset.


Input Slice

Now let’s handle user input:


import { createSlice } from '@reduxjs/toolkit';

const initialState = { inputValue: "Default Value" };

export const inputSlice = createSlice({
  name: 'input',
  initialState,
  reducers: {
    changeInputValue: (state, action) => {
      state.inputValue = action.payload;
    },
  },
});

export const { changeInputValue } = inputSlice.actions;
export default inputSlice.reducer;


Enter fullscreen mode Exit fullscreen mode

Anytime the user types something, this slice updates the Redux store with the latest value.


API Slice

Finally, let’s fetch some data from an API.


import { createSlice } from '@reduxjs/toolkit';

const initialState = { macbook: null };

const apiSlice = createSlice({
  name: 'api',
  initialState,
  reducers: {
    setMacbook: (state, action) => {
      state.macbook = action.payload;
    },
  },
});

export const { setMacbook } = apiSlice.actions;

export const fetchMacbook = () => async dispatch => {
  const response = await fetch('https://api.restful-api.dev/objects/7');
  const data = await response.json();
  dispatch(setMacbook(data));
};

export default apiSlice.reducer;


Enter fullscreen mode Exit fullscreen mode

When dispatched, this slice calls the API, retrieves MacBook details, and stores them in the Redux state.


Connecting Redux to React Native

In App.js, wrap the root component with Provider to make Redux available across the app:


import React from 'react';
import HomeScreen from './src/screens/HomeScreen';
import { Provider } from 'react-redux';
import { store } from './src/store/store';

const App = () => (
  <Provider store={store}>
    <HomeScreen />
  </Provider>
);

export default App;


Enter fullscreen mode Exit fullscreen mode

🏡 HomeScreen Implementation

Here’s how the slices come together in the UI:

  • Counter Section → Increment, decrement, reset
  • Input Section → Save and display text
  • API Section → Fetch and render MacBook details

HomeScreen Code


import { StyleSheet, Text, TextInput, View, ScrollView, TouchableOpacity } from 'react-native';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { decrement, increment, reset } from '../../store/slice/counterSlice';
import { changeInputValue } from '../../store/slice/InputSlice';
import { fetchMacbook } from '../../store/slice/apiSlice';

const HomeScreen = () => {
  const [value, setValue] = useState('');
  const { counterValue } = useSelector(state => state.counter);
  const { inputValue } = useSelector(state => state.input);
  const { macbook } = useSelector(state => state.api);
  const dispatch = useDispatch();

  return (
    <ScrollView style={styles.scrollContainer}>
      <View style={styles.container}>
        <Text style={styles.appTitle}>🚀 Redux Toolkit Demo</Text>

        {/* Counter Section */}
        <View style={styles.smallCard}>
          <Text style={styles.smallCardTitle}>🔢 Counter: {counterValue}</Text>
          <View style={styles.buttonRow}>
            <TouchableOpacity style={[styles.smallButton, styles.incrementButton]} onPress={() => dispatch(increment())}>
              <Text style={styles.smallButtonText}>+</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.smallButton, styles.decrementButton]} onPress={() => dispatch(decrement())}>
              <Text style={styles.smallButtonText}>-</Text>
            </TouchableOpacity>
            <TouchableOpacity style={[styles.smallButton, styles.resetButton]} onPress={() => dispatch(reset())}>
              <Text style={styles.smallButtonText}>Reset</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* Input Section */}
        <View style={styles.smallCard}>
          <Text style={styles.smallCardTitle}>📝 Stored: {inputValue || 'None'}</Text>
          <View style={styles.inputRow}>
            <TextInput
              style={styles.compactInputStyle}
              value={value}
              onChangeText={setValue}
              placeholder="Enter text..."
              placeholderTextColor="#999"
            />
            <TouchableOpacity 
              style={[styles.smallButton, styles.saveButton]} 
              onPress={() => {
                dispatch(changeInputValue(value));
                setValue('');
              }}
            >
              <Text style={styles.smallButtonText}>Save</Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* API Section */}
        <View style={styles.card}>
          <Text style={styles.cardTitle}>🌐 API Fetch</Text>
          <TouchableOpacity 
            style={[styles.button, styles.apiButton]} 
            onPress={() => dispatch(fetchMacbook())}
          >
            <Text style={styles.buttonText}>📡 Fetch MacBook Data</Text>
          </TouchableOpacity>

          {macbook && (
            <View style={styles.macbookContainer}>
              <Text style={styles.macbookTitle}>💻 {macbook.name}</Text>
              <View style={styles.specRow}>
                <Text style={styles.specLabel}>📅 Year:</Text>
                <Text style={styles.specValue}>{macbook.data.year}</Text>
              </View>
              <View style={styles.specRow}>
                <Text style={styles.specLabel}>💰 Price:</Text>
                <Text style={styles.specValue}>${macbook.data.price}</Text>
              </View>
              <View style={styles.specRow}>
                <Text style={styles.specLabel}> CPU:</Text>
                <Text style={styles.specValue}>{macbook.data['CPU model']}</Text>
              </View>
              <View style={styles.specRow}>
                <Text style={styles.specLabel}>💾 Storage:</Text>
                <Text style={styles.specValue}>{macbook.data['Hard disk size']}</Text>
              </View>
            </View>
          )}
        </View>
      </View>
    </ScrollView>
  );
};

export default HomeScreen;

const styles = StyleSheet.create({
  scrollContainer: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  container: {
    flex: 1,
    padding: 20,
    paddingTop: 60,
  },
  appTitle: {
    fontSize: 32,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 30,
    color: '#2c3e50',
    textShadowColor: 'rgba(0,0,0,0.1)',
    textShadowOffset: { width: 1, height: 1 },
    textShadowRadius: 2,
  },
  card: {
    backgroundColor: 'white',
    borderRadius: 16,
    padding: 20,
    marginBottom: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 5,
  },
  smallCard: {
    backgroundColor: 'white',
    borderRadius: 12,
    padding: 15,
    marginBottom: 15,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.08,
    shadowRadius: 4,
    elevation: 3,
  },
  cardTitle: {
    fontSize: 20,
    fontWeight: '700',
    marginBottom: 15,
    color: '#34495e',
    textAlign: 'center',
  },
  smallCardTitle: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 10,
    color: '#34495e',
  },
  counterDisplay: {
    alignItems: 'center',
    marginBottom: 20,
    backgroundColor: '#ecf0f1',
    borderRadius: 50,
    width: 100,
    height: 100,
    justifyContent: 'center',
    alignSelf: 'center',
  },
  counterValue: {
    fontSize: 36,
    fontWeight: 'bold',
    color: '#2c3e50',
  },
  buttonRow: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    marginTop: 10,
  },
  button: {
    paddingVertical: 12,
    paddingHorizontal: 20,
    borderRadius: 25,
    alignItems: 'center',
    justifyContent: 'center',
    minWidth: 80,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.2,
    shadowRadius: 4,
    elevation: 3,
  },
  incrementButton: {
    backgroundColor: '#27ae60',
  },
  decrementButton: {
    backgroundColor: '#e74c3c',
  },
  resetButton: {
    backgroundColor: '#f39c12',
  },
  saveButton: {
    backgroundColor: '#3498db',
    marginTop: 15,
  },
  apiButton: {
    backgroundColor: '#9b59b6',
  },
  buttonText: {
    color: 'white',
    fontWeight: '600',
    fontSize: 16,
  },
  smallButton: {
    paddingVertical: 8,
    paddingHorizontal: 15,
    borderRadius: 20,
    alignItems: 'center',
    justifyContent: 'center',
    minWidth: 60,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.15,
    shadowRadius: 2,
    elevation: 2,
  },
  smallButtonText: {
    color: 'white',
    fontWeight: '600',
    fontSize: 14,
  },
  inputRow: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 10,
  },
  compactInputStyle: {
    flex: 1,
    borderWidth: 1,
    borderColor: '#bdc3c7',
    borderRadius: 8,
    padding: 10,
    fontSize: 14,
    backgroundColor: '#fff',
  },
  storedValueContainer: {
    backgroundColor: '#ecf0f1',
    padding: 15,
    borderRadius: 10,
    marginBottom: 15,
  },
  label: {
    fontSize: 14,
    color: '#7f8c8d',
    marginBottom: 5,
  },
  storedValue: {
    fontSize: 16,
    fontWeight: '600',
    color: '#2c3e50',
  },
  inputStyle: {
    borderWidth: 2,
    borderColor: '#bdc3c7',
    borderRadius: 12,
    padding: 15,
    fontSize: 16,
    backgroundColor: '#fff',
    marginBottom: 5,
  },
  macbookContainer: {
    backgroundColor: '#f8f9fa',
    borderRadius: 12,
    padding: 20,
    marginTop: 20,
    borderLeftWidth: 5,
    borderLeftColor: '#3498db',
  },
  macbookTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#2c3e50',
    marginBottom: 15,
    textAlign: 'center',
  },
  specRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#ecf0f1',
  },
  specLabel: {
    fontSize: 16,
    color: '#7f8c8d',
    fontWeight: '500',
  },
  specValue: {
    fontSize: 16,
    color: '#2c3e50',
    fontWeight: '600',
  },
});


Enter fullscreen mode Exit fullscreen mode

👉 The UI reacts automatically when the Redux state updates.



🎯 Final Output

  • Tap +/- buttons → Counter updates
  • Enter text → Stored in Redux
  • Press Fetch button → MacBook data appears with name, year, price, CPU, and storage

✅ Why Redux Toolkit?

  • Less boilerplate than traditional Redux
  • Built-in Immer for immutability
  • Cleaner async thunks for API calls
  • Scalable for large apps

🚀 Conclusion

Redux Toolkit makes state management in React Native easier, cleaner, and scalable.
With just a few slices, we built a counter, input manager, and API fetcher — all in a structured, maintainable way.

If you’re starting a new project or refactoring an old one, Redux Toolkit is the way to go!

Top comments (0)