Mod 5 React Native #2: React Native + Redux

In this post I am going to walk you through a quick SignUp implementation using React Native, Redux and AsyncStorage

Redux

Our current curriculum does not spend much time on Redux. In my opinion and from what I hear, it is a must learn for your professional career. There is a learning curve with Redux, and it can be quite frustrating, but once you understand the conventions + philosophy, it will improve your overall programming experience. *Disclaimer* even the creator of Redux warns against using Redux for every project. I recommend using Redux if your app has any flows that require passing data through multiple screens. Below, I would like to go through the convention that helped me understand Redux and explain some of the nuances so I can hopefully save you some time and frustration.

Getting Started

Let’s get started by installing Redux and the dependancies we will be using. In your project directory run the following commands:

yarn add redux
yarn add react-redux
yarn add redux-thunk

To piggy back off the last post, let’s jump right into the flow of Redux rather than tip-toe around the philosophy. Let’s refactor the sign-up form into it’s own component and render it within its own ‘screen’ (more on screens later when we cover React Native Navigation).

mkdir Screens
mkdir Components
cd Components
touch SignUpForm.js
cd ..
cd Screens
touch SignUpScreen.js
cd ..

/Components/SignUpForm.js:

import React from "react";
import { StyleSheet, Text, View, SafeAreaView, TextInput, Button } from "react-native";
import _ from "lodash";
class SignUpForm extends React.Component {
state = {
email: "",
name: "",
password: ""
};
handleSubmit = () => {
if (this.state.email && this.state.name && this.state.password) {
const user = _.clone(this.state);
console.log(user);
}
};
render() {
return (
<SafeAreaView style={styles.container}>
<Text>Sign Up!</Text>
<View style={styles.formContainer}>
<TextInput
placeholder="Email"
onChangeText={email => this.setState({ email })}
style={styles.input}
autoCapitalize="none"
/>
<TextInput
placeholder="Name"
onChangeText={name => this.setState({ name })}
style={styles.input}
autoCapitalize="none"
/>
<TextInput
placeholder="Password"
onChangeText={password => this.setState({ password })}
style={styles.input}
secureTextEntry
/>
<Button title="Submit" onPress={this.handleSubmit} />
</View>
</SafeAreaView>
);
}
}
export default SignUpForm;const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#e6e6e6",
alignItems: "center",
justifyContent: "center"
},
formContainer: {
alignItems: "center",
width: "100%"
},
input: {
backgroundColor: "white",
padding: 10,
margin: 10,
width: "80%"
}
});

/Screens/SignUpScreen.js

import React from "react";
import { SafeAreaView, StyleSheet } from "react-native";
import SignUpForm from "../Components/SignUpForm";'
class SignUpScreen extends React.Component {
render() {
return (
<SafeAreaView style={styles.container}>
<SignUpForm />
</SafeAreaView>
);
}
}
export default SignUpScreen;const styles = StyleSheet.create({
container: {
height: "100%"
}
});

App.js

import React from "react";
import { SafeAreaView } from "react-native";
import SignUpScreen from "./Screens/SignUpScreen";
class App extends React.Component {
render() {
return (
<SafeAreaView style={styles.container}>
<SignUpScreen />
</SafeAreaView>
);
}
}
export default App;const styles = StyleSheet.create({
container: {
backgroundColor: "#e6e6e6",
height: "100%"
}
});

*Since the form is now being rendered within “SignUpScreen” we will also need to update the style for both App.js and SignUpScreen.js*

Redux Convention

Now that our SignUpForm component is ready to send a request to the backend, lets go over the sequence that you will need to complete each time you want to dispatch an action via user interaction.

  1. Setup the component and prepare it to dispatch an action
  • We have completed this step by creating the SignUpForm component.

2. Create a constant variable that will be passed as the type in the action and identified in the reducer to delegate data to the store.

  • example: USER_SIGN_UP

3. Create an action — use payload to send data to the corresponding reducer

  • example: dispatch({ type: USER_SIGN_UP, payload: response )}

4. Within your reducer, delegate the data you would like to store by action type

5. Using Redux’s higher order component, connect, mapDispatchToProps within your SignUpForm component

6. In your onSubmit, invoke the sign up action passing the user’s form input.

— — — — — — — — — — — — — — — -

2. Create a Constant

Your Redux actions + reducers can get pretty convoluted, so if there is an opportunity to add some organization, take it. Let’s create a Constants directory then within it create a actionCreator.js file. In your terminal, run the following command:

mkdir Constants
cd Constants
touch actionCreators.js
cd ..

Now within actionCreator.js, lets create a variable for type that we can use in our actions + reducers:

./Constants/actionCreators.js

// URL
export const URL = "http://localhost:3000/"
// AUTH
export const USER_SIGN_UP = "USER_SIGN_UP"

*create a constant variable that points to your server that you will make requests too, for this example, http://localhost:3000/ — this will save you a lot of time in the future if you want to show off your React Native app on device rather than on your computer.*

3. Create an action

Now let’s create the POST request to our backend through an action. In your terminal run the following commands:

mkdir Actions
cd Actions
touch auth.js
cd ..

Open up auth.js so we can create our request. With Redux we have to use a ‘thunk’ for asynchronous functions. Our request is going to look something like this:

./Actions/auth.js

import { URL, USER_SIGN_UP } from '../Constants/actionCreator'// since we are sending an object to the backend(the user's form input), we need to pass our action an argument: userexport function userSignUp(user) {
return function(dispatch) {
return fetch(`${URL}users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(user)
})
.then(response => response.json())
.then(response => {

// lets send the response from our backend
// to our AuthReducer through payload
dispatch({ type: USER_SIGN_UP, payload: response });
});
};
}

Perfect! The dispatch now sends an object containing a type and with data data attached to our reducers. Now let’s setup our AuthReducer to delegate what to store and where.

4. Create a Reducer

Now that we have our actions setup and response ready to be stored, let’s create our AuthReducer + RootReducer. In your terminal run the following commands:

mkdir Reducers
cd Reducers
touch index.js
touch AuthReducer.js
cd ..

Open up your AuthReducer.js to get started. Ohans Emannuel had a great, conceptual analogy in his eBook Understanding Redux, I highly recommend checking it out. Within your AuthReducer.js lets create our initial state as well as a switch statement that will delegate which data to save and where.

./Reducers/AuthReducer.js

import { USER_SIGN_UP } from "../Constants/actionCreator";const INIT_STATE = {
user: {}
};
function authReducer(state = INIT_STATE, action) {
switch (action.type) {
case USER_SIGN_UP:
return { ...state, user: action.payload };
default:
return state;
break;
}
}
export { authReducer }

Alright, let’s break this down.

  1. import the constant associated with the action, in this case USER_SIGN_UP
  2. create an Initial State object
  3. For our example, our reducer will take two arguments
  • initial state
  • action

4. Reducers are not supposed to perform any logic. Reducers are strictly for updating your store.

5. I love switch statements, but feel free to use whichever conditional you are most comfortable with.

6. This switch statement reads as following:

if action.type === USER_SIGN_UP, then return the existing state and update the user object with the payload passed from our action. If you take a look back at our action for sign up you will see we provided the type + payload.

../Actions/auth.jsdispatch({ type: USER_SIGN_UP, payload: response })

7. Just like working with React, you do not directly mutate state. State is read only. If we have other key / value pairs within our state object, the spread operator keeps them intact so we can explicitly update the user object only.

return { ...state, user: action.payload };

9. *Your reducer needs to return some form of state otherwise it will break. In this example, the default case will return state which is === INIT_STATE*

switch (action.type) {
case USER_SIGN_UP:
return { ...state, user: action.payload };
default:
return state;
break;
}

10. Make sure to export your authReducer function so that we can use them we can use combineReducers within RootReducer.

export { authReducer }

RootReducer

If you remember from lecture, when you createStore, you can only pass it one reducer. As your app grows, so will your actions + reducers. To combat a lengthy reducer file, Redux provides us with a combineReducers function that will create a single Reducer object that we can access throughout the app. Open up ./Reducers/index.js to get started.

./Reducers/index.js

import { combineReducers } from "redux";
import { authReducer } from './AuthReducer/'
export const rootReducer = combineReducers({
auth: authReducer
})
  1. Import combineReducers from Redux
  2. Import your authReducer
  3. Create your rootReducer

If, for example, we decided to implement some type of friend request actions / reducers, we could simply add them to the rootReducer:

import { combineReducers } from "redux";
import { authReducer } from './AuthReducer/'
import { friendshipReducer } from './FriendshipReducer'
export const rootReducer = combineReducers({
auth: authReducer,
friendship: friendshipReducer
})

Now whenever we want to access a specific store throughout our app we can just refer to this rootReducer object. You will see how valuable this is in a bit.

The Store

As you probably know by now, your ‘store’ is your global state. Using some built in Redux magic, you can access that store throughout your app. Now that we have setup an initial action and reducer let’s connect our app to Redux. To do that let’s jump into our App.js or Index.js, whichever file is highest on your component tree.

App.js

import React from "react";
import { SafeAreaView, StyleSheet } from "react-native";
import SignUpScreen from "./Screens/SignUpScreen";
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { rootReducer } from "./Reducers/index.js";
import { Provider } from "react-redux";
// storeEnhancers make time travel possible without the app being
// aware it is happening. While it might not be super relevant to
// your React Native app, it is a valuable tool to use.
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;// make sure to create your store outside of the component
const store = createStore(rootReducer, storeEnhancers(applyMiddleware(thunk)))
class App extends React.Component {
render() {
return (
<Provider store={store}>
<SafeAreaView style={styles.container}>
<SignUpScreen />
</SafeAreaView>
</Provider>
);
}
}
export default App;const styles = StyleSheet.create({
container: {
backgroundColor: "#e6e6e6",
height: "100%"
}
});

We are in business! Now all we have to do is mapDispatchToProps and invoke the action that sends a request to the backend, then store the response with Redux. Head back to ./Components/SignUpForm.js:

5. mapDispatchToProps

Now we are going to do just that, map the dispatch that will invoke the action we just created, to our props on the SignUpForm component. To do that we need to import connect from react-redux, import our userSignUp action then create our mapDispatchToProps.

./Components/SignUpForm.js

import React from "react";
import { StyleSheet, Text, View, SafeAreaView, TextInput, Button } from "react-native";
import _ from "lodash";
import { connect } from "react-redux"
import { userSignUp } from "../Actions/auth";
class SignUpForm extends React.Component {
state = {
email: "",
name: "",
password: ""
};
handleSubmit = () => {
if (this.state.email && this.state.name && this.state.password) {
const user = _.clone(this.state);
console.log(user);
}
};
render() {
return (
<SafeAreaView style={styles.container}>
<Text>Sign Up!</Text>
<View style={styles.formContainer}>
<TextInput
placeholder="Email"
onChangeText={email => this.setState({ email })}
style={styles.input}
autoCapitalize="none"
/>
<TextInput
placeholder="Name"
onChangeText={name => this.setState({ name })}
style={styles.input}
autoCapitalize="none"
/>
<TextInput
placeholder="Password"
onChangeText={password => this.setState({ password })}
style={styles.input}
secureTextEntry
/>
<Button title="Submit" onPress={this.handleSubmit} />
</View>
</SafeAreaView>
);
}
}
const mdp = dispatch => {
return {
signUpUser: (data) => dispatch(userSignUp(data))
}
}
export default connect(null, mdp)(SignUpForm);const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#e6e6e6",
alignItems: "center",
justifyContent: "center"
},
formContainer: {
alignItems: "center",
width: "100%"
},
input: {
backgroundColor: "white",
padding: 10,
margin: 10,
width: "80%"
}
});

Breakdown:

  1. connect — Redux works by using Higher Order Components. This means that the component is intercepted and modified before it is rendered else where. connect takes four arguments, the only argument we will pass for this example is mapDispatchToProps
  2. userSignUp — we have to import the action that will be performed so that the mapDispatchToProps knows what to pass to our SignUpForm component.
  3. your mapDispatchToProps takes one argument, dispatch. You can map as many actions as you want within your mapDispatchToProps. See below.

Example:

const mdp = dispatch => {
return {
signUpUser: (data) => dispatch(userSignUp(data),
loginUser: (data) => dispatch(userLogin(data)
}
}

4. Notice that I am passing the action, signUpUser, an argument of ‘data.’ If your action will be receiving data, such as input from a form, you need to specify the amount of arguments within your mapDispatchToProps

5. To invoke mdp you will need to set up your component so that connect can wrap the component’s export statement. connect requires an argument for both mapStateToProps and mapDispatchToProps, for this example we only need mapDispatchToProps so pass ‘null’ as the first argument.

export default connect(null, mdp)(SignUpForm);

Now we are ready to invoke the action! In the handleSubmit add the following:

handleSubmit = () => {
if (this.state.email && this.state.name && this.state.password) {
const user = _.clone(this.state);
this.props.userSignUp(user);
}
};

There you go! If your backend is set up properly, you can now create new users with the form then store the response in Redux!

AsyncStorage

AsyncStorage in React Native is what LocalStorage is to React. AsyncStorage provides a way to persist data on the local machine so that even if the app closes, it will retain whatever data you store. In our example, when a user signs up, we will want to persist the response(token & user_id) in our React Native app. Lets head back to our action for userSignUp:

./Actions/auth.js

import { URL, USER_SIGN_UP } from '../Constants/actionCreator'
import { AsyncStorage } from 'react-native'
export function userSignUp(user) {
return function(dispatch) {
return fetch(`${URL}/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(user)
})
.then(response => response.json())
.then(response => {
// define asynchronous function _storeData = async () => {
try {
await AsyncStorage.setItem("loggedInUser", signedUpUser);
} catch (error) {
// Error saving data
console.log(error);
}
};
//invoke
this._storeData();
dispatch({ type: USER_SIGN_UP, payload: response });
});
};
}

Breakdown:

This asynchronous function is copied and pasted from the React Native docs. To set an item in AsyncStorage, you need to define a key / value pair. In this example we created a key of ‘loggedInUser,’ ran the response through JSON.stringify to create signedUpUser then passed that as the value. To access this item elsewhere in your app, you will have to run a similar function:

_retrieveData = async () => {
try {
const value = await AsyncStorage.getItem('TASKS');
if (value !== null) {
// We have data!!
console.log(value);
}
} catch (error) {
// Error retrieving data
}
};

A recent graduate of the immersive, software engineering program at the Flatiron School in NYC.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store