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 with typescript template:
$ npx react-native init ReduxToolkitExample --template react-native-template-typescript
Next, let's install all the necessary dependencies and typings:
$ npm i react-redux @reduxjs/toolkit
$ npm i --save-dev @types/react-redux
Basic state setup
Create state slice
Let's start with creating a simple state slice that holds a message state. Redux Toolkit provides a convenience function createSlice that helps managing state slice.
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 function simplifies this by auto-generating action types and action creators based on the names of the 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 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. Redux Toolkit provides a configureStore helper function that has good default configuration options and automatically combines reducers. Here's how it looks:
import { configureStore } from '@reduxjs/toolkit';
import messageReducer from './message';
export const store = configureStore({
reducer: {
message: messageReducer
}
});
...
Don't forget to wrap your app in Redux Provider component.
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 update it's state after we click a button.
import { useDispatch, useSelector } from 'react-redux';
...
const Message = () => {
const dispatch = useDispatch();
const { message } = useSelector((state: RootState) => 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:
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 in 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 user 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 user state slice 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 loads. 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 = useDispatch();
const { users, loading } = useSelector((state: RootState) => 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:
Full code example for this part can be found here.
Adding 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 that will return a new entity state in the following form: { ids: [], entities: {} }
To keep our loading state we can pass it to initialState, 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 use setAll to set 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. Add user selectors to our user state file.
import { RootState } from '.';
...
export const {
selectById: selectUserById,
selectIds: selectUserIds,
selectEntities: selectUserEntities,
selectAll: selectAllUsers,
selectTotal: selectTotalUsers
} = usersAdapter.getSelectors((state: RootState) => state.users);
...
Updating Users component
Finally, we can make use of these new selectors 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 = useDispatch();
const { loading } = useSelector((state: RootState) => state.users);
const users = useSelector(selectAllUsers);
...
If you need to see full example code, you can find it on our repository.
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, check out the official documentation.