08.01.2021 in entwicklung

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.

13.05.2019

Building an app that matters

How we build a psychological intervention app for FU Berlin
lesen
23.04.2019

Die besten Cross-Platform und Hybrid Apps aus Deutschland

Best Practice Beispiele und Showcases aus dem deutschen Markt
lesen
11.04.2019

How to build an app?

A step-by-step guide from kickoff to submission
lesen
13.05.2019

Building an app that matters

How we build a psychological intervention app for FU Berlin
lesen
23.04.2019

Die besten Cross-Platform und Hybrid Apps aus Deutschland

Best Practice Beispiele und Showcases aus dem deutschen Markt
lesen

Wir helfen Ihnen gerne

Wir sind gespannt auf Ihr Projekt! Schreiben Sie uns eine Nachricht und wir melden uns innerhalb weniger Stunden zurück. Oder rufen Sie uns einfach an!

hello@hybridheroes.de
+49 30 23390434
Sprechen Sie uns an!