Build an image browsing App with React Native (2)— Redux

Kai Xie
5 min readMay 29, 2021

--

React Native brings React’s declarative UI framework to iOS and Android. With React Native, you use native UI controls and have full access to the native platform. (https://github.com/facebook/react-native)

We have built up an Image browsing App at

and implemented all essential functions such as fetching image data from backend API, displaying the image in a list, navigating to the detail page of a single image while the user clicks it from the list.

And in this article, I would add the whole state store based on React-Redux.

Redux is a predictable state container for JavaScript applications

React Redux is the official Redux UI binding library for React. If you are using Redux and React together, you should also use React Redux to bind these two libraries.

We would introduce the Redux store to save the state, and we would also add the Action and Reducer accordingly.

Prerequisite

Before start reading this tutorial, I assume you have some brief knowledge about Redux, or I’d suggest you look into the Redux document first.

Add Redux package

Firstly, we need to add a Redux package with the following command

yarn add react-redux

and then you would notice a new package dependency was added into the Package.json file.

Define types

And secondly, we need to create two files, Action.d.ts and State.d.ts in the @types folder and defined the following interface FetchAction in Action.d.ts

interface FetchAction {
type: string;
payload: Data | Error | undefined;
}

and the interface ReducerStateType in State.d.ts as the following:

interface ReducerStateType {
data: Data | undefined;
error: Error | undefined;
}

These two interfaces define the type of action and the type of state.

Define Actions

And then we need to define some actions. I would like to put all Redux related code, like actions, reducers in a separate folder to make the project tidy. So, let’s create a folder, redux in the src and create a new file, Action.ts in this folder, and implement two actions as following:

export const FETCH_SUCCEED = 'FETCH_SUCCESS';
export const FETCH_FAILED = 'FETCH_FAILED';
export const fetchSucceed = (data: Data): FetchAction => ({
type: FETCH_SUCCEED,
payload: data,
});
export const fetchFailed = (error: Error): FetchAction => ({
type: FETCH_FAILED,
payload: error,
});

As shown above, we defined two actions, fetchSucceed, which indicates data fetching is successful; and fetchFailed, which means the data fetching is failed. We would dispatch these actions at the proper time.

Create Reducer

Another important part of the Redux process is the reducer, so let’s create a new file, Reducer.ts in redux folder, and add the following code

import {combineReducers} from 'redux';
import {FETCH_FAILED, FETCH_SUCCEED} from '../redux/Action';
const INITIAL_STATE: ReducerStateType = {
data: undefined,
error: undefined,
};
const isData = (object: any): object is Data => object;const fetchReducer = (
state = INITIAL_STATE,
action: FetchAction,
): ReducerStateType => {
switch (action.type) {
case FETCH_SUCCEED:
if (isData(action.payload)) {
const ids: Set<number> = new Set(
state.data?.photos.map(item => item.id),
);
const newPhotos: Photo[] = action.payload?.photos?.filter(
(item: Photo) => !ids.has(item.id),
);
state = {
...state,
data: {
...action.payload,
photos:
state.data?.photos.concat(...newPhotos) || action.payload?.photos,
},
error: undefined,
};
}
else if (!action.payload) {
state = {
...state,
data: undefined,
error: undefined,
};
}
break;
case FETCH_FAILED:
if (action.payload instanceof Error) {
state = {
...state,
data: undefined,
error: action.payload,
};
};
break;
}
return state;
};
export default combineReducers({
fetch: fetchReducer,
});

It’s pretty simple and straightforward, just implemented a reducer to handle two actions, FETCH_SUCCEED and FETCH_FAILED. It would update the state.data with newly fetched data if it received a FETCH_SUCCEED action, or update the state.error if it receives a FETCH_FAILED action. There might be other ways to handle the actions, you can try to implement your approach.

Create Redux store

And then, we need to create a global Redux store, so let’s open App.tsx and add the following dependencies

import {createStore} from 'redux';
import {Provider as ReduxProvider} from 'react-redux';
import reducer from './redux/Reducer';

and create the Redux store with

export const store = createStore(reducer);

and wrap our DOM tree with this store like

<NavigationContainer>
<ReduxProvider store={store}>
<SafeAreaProvider>
<Stack.Navigator initialRouteName={'Home'}>
<Stack.Screen
name={'Home'}
component={Home}
options={{title: 'Overview'}}
/>
<Stack.Screen name={'Detail'} component={Detail} />
</Stack.Navigator>
</SafeAreaProvider>
</ReduxProvider>
</NavigationContainer>

Now, we put the whole DOM tree under a Redux store, which handles the actions with our reducer.

Update the Home component

Then, at last, we need to update the Home component to let it dispatches actions instead of setting data directly.

Open the Home.tsx and we need to add some dependencies

import {useSelector, useDispatch} from 'react-redux';
import {FETCH_SUCCEED, FETCH_FAILED} from '../redux/Action';

and getting data/error with useSelector and instantiate a dispatch with useDispatch

const Home = ({navigation}) => {
const data: Data = useSelector(state => state.fetch.data);
const error: Error = useSelector(state => state.fetch.error);
const dispatch = useDispatch();
const [keyword, setKeyword] = useState<string>();

and then update the fetchData function as shown

const fetchData = async (
query: string = '',
pageIndex: number = 0,
perPage: number = 20,
) => {
......
const json = await response.json();
dispatch({type: FETCH_SUCCEED, payload: json});
} catch (error) {
dispatch({type: FETCH_FAILED, payload: error});
} finally {
requests.delete(url);
}
};

as we see, we would dispatch some actions depending on we fetch the data successfully or failed.

and we also need to update the onSearch function as following

const onSearch = (query: string) => {
setKeyword(query);
dispatch({type: FETCH_SUCCEED, payload: undefined});
};

pretty same, we would dispatch an action with an undefined payload to reset the state.data instead of setting data to undefined directly.

Now let’s run the app again. actually, you would not find any difference because we didn’t change the functionality, we just implement it with another approach.

Then, we have added Redux to handle the data in a global state store. This change makes the data flow more flexible, and we can separate some functionality code from the container. We would see the advantage in the next tutorial.

In the next tutorial at

We would introduce persistent storage to save fetched data. And we also refactor the code according to the single source of truth principle.

Thanks for reading this tutorial and any feedback are welcome.

--

--