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.