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
- Things to remember
-
Create an async thunk using
createAsyncThunk
- Payload creator techniques
-
Add Redux to a component with
useSelector
anduseDispatch
hooks - Updating and deleting state items
- Things to check when your having a bad day
Quick setup steps
- Define the initial state by creating a constant object with the initial values for the slice's state.
-
Create the slice using
createSlice
, providing the initial state and reducers. - Export the reducer function from the slice for use in the Redux store.
-
Export any actions that were generated by
createSlice
for use in your components. -
Add the slice's reducer to the store in the
reducer
field of theconfigureStore
. - Use
useSelector
to access the slice's state in your components. - Use
useDispatch
to dispatch actions to the slice from your components.
Things to remember
- An action within a reducer contains two properties:
type
andpayload
.- 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.
- The
createAsyncThunk
Create an async thunk using - Import
createAsyncThunk
from@reduxjs/toolkit
. - Define an async thunk using
createAsyncThunk
to handle asynchronous logic including the action type string and the "payload creator" function(s). - Update the
extraReducers
in the slice to include the async thunk includingpending
,fulfilled
, andrejected
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
}
);
useSelector
and useDispatch
hooks
Add Redux to a component with // 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
andaction
. - The
action
object contains apayload
with theid
of the item to be removed. - The
findIndex
method is used to find the index of the item in thestate
array based on itsid
. - If
itemIndex
is found (i.e., not-1
), thesplice
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.
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);