Using Redux with React Hooks in a React Native app
With React Hooks growing usage, the ability to handle a component's state and side effects is now a common pattern in the functional component. React Redux offers a set of Hook APIs as an alternative to the omnipresent connect()
High Order Component.
In this tutorial, let us continue to build a simple React Native app where a user can save their notes and let use Redux Hooks API to manage state. This post is in continuation of the previous post here.
If you are familiar with the basics of React Hooks and how to implement them with a basic navigation setup, you can skip the previous post and can continue from this one.
Table of Contents
- Installing redux
- Adding action types and creators
- Add a reducer
- Configuring a redux store
- Accessing global state
- Dispatching actions
- Running the app
- Conclusion
Installing redux
If you have cloned the repo from the previous example, make sure that the dependencies
in the package.json
file looks like below:
"dependencies": {
"@react-native-community/masked-view": "0.1.5",
"expo": "~36.0.0",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz",
"react-native-gesture-handler": "~1.5.0",
"react-native-paper": "3.4.0",
"react-native-reanimated": "~1.4.0",
"react-native-safe-area-context": "0.6.0",
"react-native-screens": "2.0.0-alpha.12",
"react-navigation": "4.0.10",
"react-navigation-stack": "2.0.10",
"react-redux": "7.1.3",
"redux": "4.0.5"
},
Next, install the following dependencies from a terminal window to integrate and use Redux to manage the state.
yarn add redux react-redux lodash.remove
The directory structure that I am going to follow to manage Redux related files is going to be based on the pragmatic approach called ducks. Here is the link to a great post on using ducks pattern in Redux and React apps. This post can help you understand the pattern and why there can be a requirement for it.
What ducks pattern allows you to have are modular reducers in the app itself. You do not have to create different files for actions, types, and action creators. Instead, you can define them all in one modular file however, if there is a need to create more than one reducer, you can have defined multiple reducer files.
Adding action types and creators
When using Redux to manage the state of the whole application, the state itself is represented by one JavaScript object. Think of this object as read-only, since you cannot make changes to this state (which is represented in the form of a tree) directly. It requires actions to do so.
Actions are like events in Redux. They can be triggered in the button press, timers or network requests.
To begin, inside the src/
directory, create a subdirectory called redux
. Inside it, create a new file called notesApp.js
.
So far, the application has the ability to let the user add notes. In the newly created files, let us begin by defining two action types and their creators. The second action type is going to allow the user to remove an item from the ViewNotes
screen.
// Action Types
export const ADD_NOTE = 'ADD_NOTE';
export const DELETE_NOTE = 'DELETE_NOTE';
Next, let us define action creators for each of the action type. The first one is going trigger when saving the note. The second creator is going to trigger when deleting the note.
// Action Creators
let noteID = 0;
export function addnote(note) {
return {
type: ADD_NOTE,
id: noteID++,
note
};
}
export function deletenote(id) {
return {
type: DELETE_NOTE,
payload: id
};
}
Add a reducer
The receiver of the action is known as a reducer. Whenever an action is triggered, the state of the application changes. The handling of the application’s state is done by the reducers.
A reducer is a pure function that calculates the next state based on the initial or previous state. It always produces the same output if the state is unchanged. It takes two inputs, the state
and action
and must return the default state
.
The initial state is going to be an empty array. Add the following after you have defined action creators. Also, make sure to import remove
utility from lodash.remove
npm package at the top of the file notesApp.js
that was installed at the starting of this post.
// import the dependency
import remove from 'lodash.remove';
// reducer
const initialState = [];
function notesReducer(state = initialState, action) {
switch (action.type) {
case ADD_NOTE:
return [
...state,
{
id: action.id,
note: action.note
}
];
case DELETE_NOTE:
const deletedNewArray = remove(state, obj => {
return obj.id != action.payload;
});
return deletedNewArray;
default:
return state;
}
}
export default notesReducer;
Configuring a redux store
A store is an object that brings and actions and reducers together. It provides and holds state at the application level instead of individual components. Redux is not an opinionated library in terms of which framework or library should use it or not.
With the creation of reducer done, create a new file called store.js
inside src/redux/
. Import the function createStore
from redux
as well as the only reducer in the app for now.
import { createStore } from 'redux';
import notesReducer from './notesApp';
const store = createStore(notesReducer);
export default store;
To bind this Redux store in the React Native app, open the entry point file App.js
and import the store
as well as the High Order Component Provider
from react-redux
npm package. This HOC helps you to pass the store down to the rest of the components of the current app.
import React from 'react';
import { Provider as PaperProvider } from 'react-native-paper';
import AppNavigator from './src/navigation';
import { Provider as StoreProvider } from 'react-redux';
import store from './src/redux/store';
// modify the App component
export default function App() {
return (
<StoreProvider store={store}>
<PaperProvider>
<AppNavigator />
</PaperProvider>
</StoreProvider>
);
}
That's it! The Redux store is now configured and ready to use.
Accessing global state
To access state when managing it with Redux, useSelector
hook is provided. It is similar to mapStateToProps
argument that is passed inside the connect()
. It allows you to extract data from the Redux store state using a selector function.
The major difference between the hook and the argument is that the selector may return any value as a result, not just an object.
Open ViewNotes.js
file and import this hook from react-redux
.
// ...after rest of the imports
import { useSelector } from 'react-redux';
Next, instead of storing notes
array using useState
Hook, replace it with the following inside ViewNotes
functional component.
const notes = useSelector(state => state);
Dispatching actions
The useDispatch()
hook completely refers to the dispatch function from the Redux store. This hook is used only when there is a need to dispatch an action. Import it from react-redux
and also, the action creators addnote
and deletenote
from the file redux/notesApp.js
.
import { useSelector, useDispatch } from 'react-redux';
To dispatch an action, define the following statement after the useSelector
hook.
const dispatch = useDispatch();
Next, dispatch two actions called addNote
and deleteNote
to trigger these events.
const addNote = note => dispatch(addnote(note));
const deleteNote = id => dispatch(deletenote(id));
Since the naming convention is exactly the same for the addNote
action as from the previous post's helper function, there is no need to make any changes inside the return
statement of the functional component for this. However, deleteNote
action is new.
To delete a note from the list rendered, add a prop onPress
to List.Item
UI component from react-native-paper
. This is going to add the functionality of deleting an item from the list when the user touches that item.
Here is the code snippet of List.Item
component with changes. Also, make sure that to modify the values of props: title
and description
.
<List.Item
title={item.note.noteTitle}
description={item.note.noteValue}
descriptionNumberOfLines={1}
titleStyle={styles.listTitle}
onPress={() => deleteNote(item.id)}
/>
The advantage useDispatch
hook provides is that it replaces mapDispatchToProps
and there is no need to write boilerplate code to bind action creators with this hook now.
Running the app
So far so good. Now, let us run the application. From the terminal window execute the command expo start
or yarn start
and make sure the Expo client is running on a simulator or a real device. You are going to be welcomed by the following home screen that currently has no notes to display.
Here is the complete demo that showcases both adding a note and deleting a note functionality.
For you reference, there are no changes made inside the AddNotes.js
file and it still uses the useState
to manage the component's state. There are quite a few changes made to ViewNotes.js
file so here is the complete snippet of code:
// ViewNotes.js
import React from 'react';
import { StyleSheet, View, FlatList } from 'react-native';
import { Text, FAB, List } from 'react-native-paper';
import { useSelector, useDispatch } from 'react-redux';
import { addnote, deletenote } from '../redux/notesApp';
import Header from '../components/Header';
function ViewNotes({ navigation }) {
// const [notes, setNotes] = useState([])
// const addNote = note => {
// note.id = notes.length + 1
// setNotes([...notes, note])
// }
const notes = useSelector(state => state);
const dispatch = useDispatch();
const addNote = note => dispatch(addnote(note));
const deleteNote = id => dispatch(deletenote(id));
return (
<>
<Header titleText="Simple Note Taker" />
<View style={styles.container}>
{notes.length === 0 ? (
<View style={styles.titleContainer}>
<Text style={styles.title}>You do not have any notes</Text>
</View>
) : (
<FlatList
data={notes}
renderItem={({ item }) => (
<List.Item
title={item.note.noteTitle}
description={item.note.noteValue}
descriptionNumberOfLines={1}
titleStyle={styles.listTitle}
onPress={() => deleteNote(item.id)}
/>
)}
keyExtractor={item => item.id.toString()}
/>
)}
<FAB
style={styles.fab}
small
icon="plus"
label="Add new note"
onPress={() =>
navigation.navigate('AddNotes', {
addNote
})
}
/>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
paddingHorizontal: 10,
paddingVertical: 20
},
titleContainer: {
alignItems: 'center',
justifyContent: 'center',
flex: 1
},
title: {
fontSize: 20
},
fab: {
position: 'absolute',
margin: 20,
right: 0,
bottom: 10
},
listTitle: {
fontSize: 20
}
});
export default ViewNotes;
Conclusion
With the addition to hooks such as useSelector
and useDispatch
not only reduces the need to write plentiful boilerplate code but also gives you the advantage to use functional components.
For advanced usage of Hooks with Redux, you can check out the official documentation here.
You can find the complete code for this tutorial in the Github repo here.
Originally published at Heartbeat.fritz.ai