Redux Toolkit Quick Reference

Note: This guide does not focus on concrete implementations of specific features or functionalities. Instead, it serves as a quick reference to common tasks and operations you may encounter when working with Redux Toolkit in a React Native application.

Quick setup steps

  1. Define the initial state by creating a constant object with the initial values for the slice's state.
  2. Create the slice using createSlice, providing the initial state and reducers.
  3. Export the reducer function from the slice for use in the Redux store.
  4. Export any actions that were generated by createSlice for use in your components.
  5. Add the slice's reducer to the store in the reducer field of the configureStore.
  6. Use useSelector to access the slice's state in your components.
  7. Use useDispatch to dispatch actions to the slice from your components.

Things to remember

  • An action within a reducer contains two properties: type and payload.
    • The payload property is an object that holds the data transferred to the action.
    • the type property is a string that identifies the action type.

Create an async thunk using createAsyncThunk

  1. Import createAsyncThunk from @reduxjs/toolkit.
  2. Define an async thunk using createAsyncThunk to handle asynchronous logic including the action type string and the "payload creator" function(s).
  3. Update the extraReducers in the slice to include the async thunk including pending, fulfilled, and rejected actions.
// src/features/feature/featureSlice.js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

const initialState = {
    data: {},
    isLoading: false,
    error: null
};

export const someActionAsync = createAsyncThunk(
    'feature/someAction',
    async (value, thunkAPI) => {
        
        try {
            // Code to perform an asynchronous operation and return the data
        } catch (error) {
            return thunkAPI.rejectWithValue(error.message);
        }
    }
);

const featureSlice = createSlice({
    name: 'feature',
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(someActionAsync.pending, (state) => {
                state.loading = true;
                state.error = null;
            })
            .addCase(someActionAsync.fulfilled, (state, action) => {
                state.loading = false;
                state.error = null;
                state.data = action.payload;
            })
            .addCase(someActionAsync.rejected, (state, action) => {
                state.isLoading = false;
                state.error = action.error.message;
            });
    }
});

export default featureSlice.reducer;

Payload creator techniques

Verifying the payload creator parameter has been passed

export const fetchFeatureById = createAsyncThunk(
    'feature/fetchFeatureById',
    async (id, thunkAPI) => {
        if(!id) thunkAPI.rejectWithValue('Feature ID can not be empty');
        // Code to perform an asynchronous operation and return the data
    }
);

Handle the error in the rejected case of the extraReducers:

extraReducers: (builder) => {
    builder
        .addCase(fetchFeatureById.rejected, (state, action) => {
            state.isLoading = false;
            state.error = action.payload;
        });
}

Check if the data is already in the state (by ID)

This is dependent on the structure of your state object. If the data is stored in an object where the key is the ID of the data, you can check if the data already exists in the state before making an API call.

const initialState = {
    data: {
        '1': { id: '1', name: 'Feature 1', description: 'This is feature 1', },
        '2': { id: '2', name: 'Feature 2', description: 'This is feature 2', },
    },
};

export const fetchFeatureById = createAsyncThunk(
    'feature/fetchFeatureById',
    async (id, thunkAPI) => {
        const existing = thunkAPI.getState().feature.data[id];
        // If the feature already exists in the state, return it
        if(existing) return state.feature.data[id];

        // else, perform an asynchronous operation and return the data
    }
);

Check for existing state when store data is an object

export const fetchProductsByCategory = createAsyncThunk(
    'feature/fetchProductsByCategory',
    async (feature, thunkAPI) => {
        // If no feature is provided, reject the promise with a value of 'feature not found'
        if (!feature) return thunkAPI.rejectWithValue('feature not found');
        // Get the products for the feature from the state
        const existing = thunkAPI.getState().products.data[feature];
        // If the products for this feature already exist in the state, return them
        if (existing) return { [feature]: existing };

        // Code to perform an asynchronous operation and return the data
    }
);

Add Redux to a component with useSelector and useDispatch hooks

// src/components/MyComponent.jsx
import { useSelector, useDispatch } from 'react-redux';
import { action1, action2, selectFeatureState } from '../features/feature/featureSlice';

function MyComponent() {
    const { data, isLoading, error } = useSelector(state => state.feature);
    const dispatch = useDispatch();

    useEffect(() => {
        dispatch(someAction());
    }, []);
}

Updating and deleting state items

Update an existing item in an array of objects (TBD)


Delete an item from an array of objects (Immer)

reducers: {
    removeItemById: (state, action) => {
        const {id} = action.payload;
        const itemIndex = state.findIndex(item => item.id === id);
        if (itemIndex !== -1) {
            state.splice(itemIndex, 1);
        }
    },
},

Whats happening here?

  • The removeItemById reducer function takes two parameters: state and action.
  • The action object contains a payload with the id of the item to be removed.
  • The findIndex method is used to find the index of the item in the state array based on its id.
  • If itemIndex is found (i.e., not -1), the splice method is used to remove the item at that index from the state array.
  • The splice method modifies the state array directly by removing the specified item.
  • If itemIndex is -1 (i.e., the item is not found), no action is taken.

Why are you using splice isn't this mutating?

While it's true that splice() is a mutating method, Redux Toolkit uses the Immer library under the hood, which allows you to write "mutating" logic in a safe way. Immer works by creating a draft state and applying all mutations to the draft. The original state is left unmodified, and a new state is returned that includes the applied mutations.


Things to check when your having a bad day

Understand your state objects

Selecting the entire Redux state object (all slices):

const state = useSelector(state => state)

Selecting a specific feature slice of your state:

const featureState = useSelector(state => state.feature)

Destructuring properties data, error, and isLoading from the feature slice.

const { data, error, isLoading } = useSelector(state => state.feature);

Are you getting the correct slice?

When using the useSelector hook, make sure you are selecting the correct slice of the state object. This is the second part of the selector function and should match the name of the slice you defined in the configureStore when setting up the Redux store. For example, if you have a slice named products, the selector should look like this:

// store.js
export const store = configureStore({
    reducer: {
        products: productsReducer,
    },
});
const { data, error, isLoading } = useSelector(state => state.products);