Using Redux Toolkit in React Native: Getting started and usage guide
Redux Toolkit is an official package from the Redux Team that helps configuring Redux store and reduces boilerplate. It also includes many convenience features that help supercharge your state management. Using Redux Toolkit in React Native is straightforward, so let's setup a simple application and see it in action! You can follow along the article or if you prefer you can also find all of the example code on our GitHub repo.
Project setup
To get started, I'm going to create a new React Native project:
$ npx react-native init ReduxToolkitExample
Next, let's install all the necessary dependencies:
$ npm install react-redux @reduxjs/toolkit
Basic state setup
Create state slice
Let's start by creating a simple state slice that holds message state. Redux Toolkit provides a convenience function createSlice
that helps setting up and managing a state slice. It requires providing a name for the slice, initial state values and reducer functions that manage state updates.
import { createSlice } from "@reduxjs/toolkit"
const messageSlice = createSlice({
name: "message",
initialState: {
message: "Initial message"
},
reducers: {}
})
export default messageSlice.reducer
Normally in Redux we would have to setup a slice reducer function with action types and action creators, but createSlice
simplifies this by auto-generating action types and action creators based on the names of reducer functions. In addition, thanks to the included Immer library we can write immutable state updates with mutable code. With that in mind, let's create and export our reducer function that changes message state.
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
const messageSlice = createSlice({
name: "message",
initialState: {
message: "Initial message"
},
reducers: {
setMessage(state, action: PayloadAction<string>) {
state.message = action.payload
}
}
})
export const { setMessage } = messageSlice.actions
export default messageSlice.reducer
Export store
Now that we have our state slice, we need to create a store and add our message
slice reducer function. Redux Toolkit provides a configureStore
helper function that has good default configuration options and automatically combines reducers. Additionally, you'll want to export AppDispatch
and RootState
types that infer from the store itself, updating them as we make changes to the store. Here's how everything should look:
import { configureStore } from '@reduxjs/toolkit';
import messageReducer from './message';
export const store = configureStore({
reducer: {
message: messageReducer
}
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
It's also recommended to create a separate file that exports typed dispatch and selector hooks, which will save you from importing AppDispatch
and RootState
every time you want to use them:
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux';
import { RootState, AppDispatch } from './store';
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Lastly, wrap your app in Redux Provider component and pass down that store
export we created earlier:
import { Provider } from 'react-redux';
import { store } from './store';
...
const App = () => {
return (
<Provider store={store}>
...
</Provider>
);
};
...
Setup Message component
Finally, let's create a simple component that will select our message, display it and perform a state update after we click a button.
import { useAppDispatch, useAppSelector } from '../hooks';
...
const Message = () => {
const dispatch = useAppDispatch();
const { message } = useAppSelector(state => state.message);
const handlePress = () => {
dispatch(setMessage('Message from Component'));
};
return (
<View style={styles.container}>
<Text style={styles.text}>{message}</Text>
<Button title={'Set Message'} onPress={handlePress} />
</View>
);
};
...
Here's how it looks so far:
Using Thunk Actions
Create users state slice
That was fun! But let's step it up a notch and take a look at asynchronous operations. I would like to fetch and store user data from https://reqres.in/ API, so let's setup a new users
state slice that will contain an array of users. I'm also going to create a UserData
interface that models to the data returned by the API.
import { createSlice } from "@reduxjs/toolkit"
interface UserData {
id: number
email: string
first_name: string
last_name: string
avatar: string
}
const usersSlice = createSlice({
name: "users",
initialState: {
users: [] as UserData[],
loading: false
},
reducers: {}
})
export default usersSlice.reducer
Setup Async Thunk
Async logic in Redux generally relies on redux-thunk middleware and Redux Toolkit's configureStore
automatically sets that up for us. To use it, let's import createAsyncThunk
and create our user data fetching thunk function in users
state slice that returns parsed data.
import { createAsyncThunk } from '@reduxjs/toolkit';
...
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const response = await fetch('https://reqres.in/api/users?delay=1');
return (await response.json()).data as UserData[];
});
...
createAsyncThunk
will then generate three action creators: pending, fulfilled, and rejected. Each of these action creators will be attached to our thunk action and we can reference their action types in reducer and respond to them when they are dispatched.
Redux Toolkit also provides extraReducers
function that can respond to other action types beside the ones generated by createSlice. Let's use that here to setup our reducers that will update our users
state:
const usersSlice = createSlice({
name: "users",
initialState: {
users: [] as UserData[],
loading: false
},
reducers: {},
extraReducers: builder => {
builder.addCase(fetchUsers.pending, state => {
state.loading = true
})
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.users = action.payload
state.loading = false
})
builder.addCase(fetchUsers.rejected, state => {
state.loading = false
})
}
})
Export users reducer
Now we only need to add our new users
state slice reducer to the store and the setup is complete.
...
import usersReducer from './users';
export const store = configureStore({
reducer: {
message: messageReducer,
users: usersReducer
}
});
...
Setup Users component
Lastly, let's create a new component that will fetch and display our user data when it first renders. I'm also going to add a reload button so the data could be re-fetched whenever.
...
import { fetchUsers } from '../store/users';
const Users = () => {
const dispatch = useAppDispatch();
const { users, loading } = useAppSelector(state => state.users);
useEffect(() => {
dispatch(fetchUsers());
}, []);
if (loading) {
return <ActivityIndicator size="large" style={styles.loader} />;
}
return (
<View>
<Button title={'Reload'} onPress={() => dispatch(fetchUsers())} />
{users.map((user) => {
return (
<View style={styles.container} key={user.id}>
<View>
<View style={styles.dataContainer}>
<Text>
{user.first_name} {user.last_name}
</Text>
</View>
<View style={styles.dataContainer}>
<Text>{user.email}</Text>
</View>
</View>
</View>
);
})}
</View>
);
};
...
And this is how our User component looks in action:
Full code example for this part can be found here.
Advanced setup: Entity Adapter
Setting up initial state
That already looks really good, but there is one more improvement we can do. Redux Toolkit also provides createEntityAdapter
function that generates a set of prebuilt reducers and selectors for performing CRUD operations on a normalized state structure. Our user data is one of such structures, so we can make use of it.
Let's add entity adapter to our users
state. First create usersAdapter
that infers UserData
type definitions, then replace initialState
with adapter's getInitialState
function that will return a new entity state in the following form: { ids: [], entities: {} }
To keep our loading state we can pass it to getInitialState
, which accepts an optional object as an argument that will be merged into the returned initial state value.
import { createEntityAdapter } from '@reduxjs/toolkit';
...
export const usersAdapter = createEntityAdapter<UserData>();
const usersSlice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState({
loading: false
}),
...
Replacing state update
Entity adapter provides many CRUD functions that help with adding, updating, and removing entity instances from an entity state object. Let's update our reducer function that references fulfilled action type to use setAll
when updating our user data.
...
builder.addCase(fetchUsers.fulfilled, (state, action) => {
usersAdapter.setAll(state, action.payload);
state.loading = false;
});
...
Adding prebuilt selectors
Now that we have our entity adapter set up, we can make use of prebuilt selectors to help with selecting the content we want, which in our case is selectAll
to select an array of all users. Add and export this selector in our users
state file.
import { RootState } from '.';
...
const usersSelectors = usersAdapter.getSelectors(
(state: RootState) => state.users,
);
export const selectAllUsers = usersSelectors.selectAll;
...
Updating Users component
Finally, we can make use of this new selector in our User component. Let's select all of our users so we can keep displaying them.
...
import { fetchUsers, selectAllUsers } from '../store/users';
const Users = () => {
const dispatch = useAppDispatch();
const { loading } = useAppSelector(state => state.users);
const users = useAppSelector(selectAllUsers);
...
If you need to see full example code, you can find it on our repository.
Data fetching and caching with RTK Query
Creating API slice and adding queries
RTK Query is an additional optional tool that can simplify common data fetching patterns and caching logic. Built on top of other Redux Toolkit's APIs it helps with request's state tracking, optimistic UI updates, and cache lifetime management. Let's try making use of it in our application.
Similarly as before, we'll create a new state slice, however this time we'll use RTK Query's createApi
to make it an "API slice" that will handle data fetching. For querying we can use a new endpoint from https://reqres.in/ that returns data about colors. Define ColorData
interface and colorApi
service that includes baseQuery
and endpoints
parameters to fetch all our data. Then, export auto-generated hooks that we'll use to fetch the data in our component. Here's how it looks:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
interface ColorData {
id: number;
name: string;
year: number;
color: string;
pantone_value: string;
}
export const colorsApi = createApi({
reducerPath: 'colorsApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://reqres.in/api/' }),
endpoints: builder => ({
getColors: builder.query<{ data: ColorData[] }, void>({
query: () => 'colors?per_page=12',
}),
getColorById: builder.query<{ data: ColorData }, number>({
query: id => `colors/${id}?delay=1`,
}),
}),
});
export const { useGetColorsQuery, useGetColorByIdQuery } = colorsApi;
After that, we need to configure our store by adding a slice reducer that was auto-generated by our new API slice. Additionally, we need to include a custom middleware that enables caching and manages subscription lifetimes.
import { colorsApi } from './colors';
...
export const store = configureStore({
reducer: {
...
[colorsApi.reducerPath]: colorsApi.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(colorsApi.middleware),
});
...
And with that we're ready to call our hooks and fetch the data. I've setup a new Colors
component that uses one of the auto-generated hooks to fetch all colors as soon as it's rendered and displays appropriate UI elements throughout the lifecycle of the request, such as loading spinner or error message. Upon successfully fetching the data it will display all colors in a list with a name and a preview of the color. I also added an additional component that will render based on expanded state that is set by pressing a toggle next to each color name. Once expanded, it will fetch detailed information about the selected color using the other hook that we exported previously. Take a look below:
...
import { useGetColorsQuery } from '../../store/colors';
import ColorDetails from '../components/ColorDetails';
const Colors = () => {
const [selectedColorId, setSelectedColorId] = useState<number | null>(null);
const { data: colors, error, isFetching } = useGetColorsQuery();
if (isFetching) {
return <ActivityIndicator size="large" style={styles.loader} />;
}
if (error) {
return <Text>Error while loading data</Text>;
}
if (!colors) {
return <Text>No data</Text>;
}
return (
<View>
{colors.data.map(({ id, name, color }) => {
const isSelected = id === selectedColorId;
return (
<View style={styles.container} key={id}>
<View style={styles.colorHeaderContainer}>
<View style={styles.colorLabel}>
<View
style={[styles.colorCircle, { backgroundColor: color }]}
/>
<Text style={styles.capitalizedText}>{name}</Text>
</View>
<Button
title={isSelected ? 'Collapse' : 'Expand'}
onPress={() => setSelectedColorId(isSelected ? null : id)}
/>
</View>
{isSelected && (
<View style={styles.colorDetailsContainer}>
<ColorDetails id={selectedColorId} />
</View>
)}
</View>
);
})}
</View>
);
};
...
And here is the ColorDetails
component that renders based on expanded toggle mentioned before. Upon the first render of this component RTK Query will fetch color details from the API and start a subscription to this data in the cache. Any subsequent render of this component that subscribes to the same data will fetch the cached data instead. Furthermore, additional components that subscribe to the same endpoint and parameter combination will use cached data and won't make another identical request.
...
import { useGetColorByIdQuery } from '../../store/colors';
const ColorDetails = (props: { id: number }) => {
const { data: color, isFetching } = useGetColorByIdQuery(props.id);
if (isFetching) {
return <ActivityIndicator size="large" style={styles.loader} />;
}
if (!color) {
return <Text>No data</Text>;
}
const { name, id, year, color: hex, pantone_value } = color.data;
const renderDetails = (label: string, value: string | number) => (
<View style={styles.detailsContainer}>
<Text style={styles.label}>{label}: </Text>
<Text style={styles.capitalizedText}>{value}</Text>
</View>
);
return (
<>
{renderDetails('ID', id)}
{renderDetails('Year', year)}
{renderDetails('Name', name)}
{renderDetails('Hex Code', hex)}
{renderDetails('Pantone Value', pantone_value)}
</>
);
};
...
Full example code is available on our repository.
Cache behaviour
By default, data is kept in the cache until 60 seconds after the last active subscription for a particular endpoint and parameter combination ends. That means if we toggle the same color detail component within 60 seconds it will use cached data and won't refetch. We can see this behaviour more clearly if we reduce the subscription time with keepUnusedDataFor
in getColorById
endpoint.
export const colorsApi = createApi({
...
endpoints: builder => ({
...
getColorById: builder.query<{ data: ColorData }, number>({
query: id => `colors/${id}?delay=1`,
keepUnusedDataFor: 5,
}),
}),
});
Following video demonstrates adjusted subscription time behaviour. Notice how after first colors' details are fetched and afterwards collapsed for more than 5 seconds it will unsubscribe from the cache and re-fetch when details are expanded again towards the end of the video. However, toggling fetched colors within the unused data window will not perform the re-fetch.
There are many other options that allow manipulating cache behaviour and tailoring them to your needs. Making use of RTK Query's caching is a great way to improve your app's performance without hand-writing such logic.
Conclusion
Redux Toolkit provides many advantages when building state management solution, especially when combined with asynchronous requests and normalized data. Using it in React Native is very easy and should definitely be considered over regular Redux workflow. Hope you found this informative and if you would like to learn more about Redux Toolkit, I encourage you to check out the official documentation.
If you want to know more about using Redux Toolkit in React Native read this two-part blogpost:
Using Redux in Flutter Part 1 - Redux setup and basic state management: flutter redux example
Using Redux in Flutter Part 2 - Thunk actions: flutter redux middleware
This blogpost has been reviewed and updated as of 17.01.2024! The following changes have been made:
- Updated example project dependencies
- Updated video previews
- Added RTK Query section
- Various smaller text changes