Download as pdf or txt
Download as pdf or txt
You are on page 1of 24

Images haven’t loaded yet.

Please exit printing, wait for images to load, and try to


Building a React Native JWT Client:
print again.

API Requests and AsyncStorage


Nick West (眨眼龙) Follow
Mar 27, 2018 · 9 min read

Part III of JWT Auth with an Elixir on


Phoenix 1.4 API and React Native
In Part I, we built a blazing-fast Elixir on Phoenix Guardian JWT
authentication API. (Github Repo)

In Part II, we established our React Native app and its base
components/screens. (Github Repo)

In this third and final part, we will use Axios to make HTTP requests to
our Elixir API, and we will save relevant data to our device using React
Native’s AsyncStorage module.
While this is Part III of the Elixir/Phoenix — React Native JSON Web Token
guide, a React Native JWT client built with this guide will work with any
matching API.

POSTing User Registration


Now that we have a viable app frontend, our app needs to be able to
send user registration data to our API endpoint.

While we could use JavaScript’s fetch() method for our API requests,
we are going to use the Axios HTTP request library instead.

Why Axios instead of fetch()?


Axios returns automatically stringifed JSON responses from our HTTP
requests, whereas responses returned by fetch() have to be manually
JSON-stringified.

Axios will also catch errors properly when we receive error statuses,
whereas fetch() will return ok when it receives a error statuses (such
as 500), which would be problematic.

Let’s install axios in our app’s root directory, with yarn add axios , or
npm i --save axios if you do not have yarn installed.

Import axios in Registration.js and Login.js:

1 // Identical imports for both Registration & Login


2 import React, { Component, Fragment } from 'react';
3 import { View, Text } from 'react-native';
4 import { Input, TextLink, Loading, Button } from './common';
5 import axios from 'axios';
6

In Registration.js, create a function inside our component named


registerUser(), bind it in our constructor, de-reference our relevant
form state with const, and add a statement that sets loading and error
to true when registerUser() runs:
1 # snip
2
3 class Registration extends Component {
4 constructor(props){
5 # code omitted
6
7 this.registerUser = this.registerUser.bind(this);
8 }
9
10 registerUser() {
11 const { email, password, password_confirmation } = this

Now, add the following axios.post() function with a .then() promise


to handle JWT responses, and a .catch() function to handle errors:

1 # snip
2
3 registerUser() {
4 const { email, password, password_confirmation } = this
5
6 this.setState({ error: '', loading: true });
7
8 // NOTE HTTP is insecure, only post to HTTPS in product
9
10 axios.post("http://localhost:4000/api/v1/sign_up",{
11 user: {
12 email: email,
13 password: password,
14 password_confirmation: password_confirmation
15 }
16 },)
17 then((response) => {

As you can see above, our Registration’s axios.post() function POST sa


user: {} object containing our de-referenced state’s email, password,
and password_confirmation to our API’s “/v1/sign_up” endpoint.

Assuming our registration is successful, we need to save our JWT


response to the device somehow. We will use React Native’s
AsyncStorage functions for this.

About React Native AsyncStorage


React Native’s AsyncStorage module provides React Native apps with a
persistent key-value storage system.

On iOS, AsyncStorage stores smaller values to serialized dictionaries


and larger values in entire files. On Android, AsyncStorage will use
either RocksDB or SQLite.

While AsyncStorage is unencrypted by default, apps on your device can


only access their own AsyncStorage values (unless your device has been
jailbreaked/jailbroken). Any AsyncStorage values saved by your app
will be safe from cross-app snooping if your device has not been
jailbroken.

For more on AsyncStorage, read the React Native docs!

Saving to Device Storage with AsyncStorage


Create the file src/services/deviceStorage.js . Import {

AsyncStorage } from React Native, create an exportable const named


deviceStorage , and export default deviceStorage like so:

1 import { AsyncStorage } from 'react-native';


2
3 const deviceStorage = {
4 // our AsyncStorage functions will go here :)
5 };
6

Inside deviceStorage, create an async function named saveItem that


takes in a key and a corresponding value as arguments. Within this
async function, write a try{ await } clause that runs
AsyncStorage.setItem(key, value); and a catch(error){} clause
that logs any errors.

1 # snip
2 const deviceStorage = {
3
4 async saveItem(key, value) {
5 try {
6 await AsyncStorage.setItem(key, value);
7 } catch (error) {
8 console.log('AsyncStorage Error: ' + error.message);
9 }
The try{ await function() } clause above makes your app wait until
the function contained is completed before executing any successive
functions. (Make it to the end of this guide for a bonus note on
Asynchronous vs Synchronous functions 😉.)

Back in Register.js, let’s import our deviceStorage module:

1 import React, { Component, Fragment } from 'react';


2 import { View, Text } from 'react-native';
3 import { Input, TextLink, Loading, Button } from './common';
4 import axios from 'axios';
5 import deviceStorage from '../services/deviceStorage'; # Imp
6

Before we do any saving, let’s inspect the response to our user


registration POST in console:

axios.post("http://localhost:4000/api/v1/sign_up",{
user: {
email: email,
password: password,
password_confirmation: password_confirmation
}
},)
.then((response) => {
console.log(response);

Now, open up the remote debugger console and take a look at our
response object:
Our JWT is located at .data.jwt on our response object, so
response.data.jwt.

Let’s remove our console.log and pass the JWT part of our response
through deviceStorage.saveKey:

axios.post("http://localhost:4000/api/v1/sign_up",{
user: {
email: email,
password: password,
password_confirmation: password_confirmation
}
},)
.then((response) => {
deviceStorage.saveKey("id_token", response.data.jwt);
})

In deviceStorage.saveKey(“id_token”, response.data.jwt) , our JWT


is saved to device storage with the key of id_token. When we retrieve
our token later, we will look for an AsyncStorage value with this
id_token key.

Now, we’re saving our response JWT to local storage, but we aren’t
saving it to our root component state. Let’s do that.

Setting Parent State from Children


In order to set our App.js component’s JWT state from our
Registration or Login components, we have to create a function that
sets JWT state in App.js and bind it in App’s constructor:

1 # snip
2
3 export default class App extends Component {
4 constructor() {
5 super();
6 this.state = {
7 jwt: '',
8 loading: true
9 }
10
11 this.newJWT = this.newJWT.bind(this);
12 }
13
14 newJWT(jwt){
Still in App.js, pass newJWT(JWT) through <Auth> as a prop:

newJWT(jwt){
this.setState({
jwt: jwt
});
}

render() {
if (!this.state.jwt) {
return (
<Auth newJWT={this.newJWT}/>
);

In Auth.js, pass newJWT={this.props.newJWT} as a prop into both


Registration and Login within the whichForm() function:

1 # snip
2
3 whichForm() {
4 if(!this.state.showLogin){
5 return(
6 <Registration newJWT={this.props.newJWT} authSwitch
7 );
8 } else {
9 return(
10 <Login newJWT={this.props.newJWT} authSwitch={this.
11 )

Finally, back in Registration.js, run


this.props.newJWT(response.data.jwt) in our axios callback:

1 # snip
2
3 axios.post("http://localhost:4000/api/v1/sign_up",{
4 user: {
5 email: email,
6 password: password,
7 password_confirmation: password_confirmation
8 }
9 },)
10 .then((response) => {
11 d i St K ("id t k " d t j t)
Now, our axios.post() saves the returned JWT to device storage and
sets our JWT to our parent app state.

We accomplish this by passing the App.js newJWT(){ setState } as a


prop into through Auth screen, then again as a prop through our
Registration component inside our Auth screen component.

So our state-setting function is being passed parent to child as: App


root-> Auth -> Registration -> Auth -> App .

This is the perfect place to implement Redux, or an alternative flux


state management architecture rather than deep prop function passing,
but that is beyond the scope of this guide 😉.

POSTing User Login


In components/Login.js, import axios and deviceStorage and create
+ bind a function named loginUser() inside the Login component
that POST s our user login email/password state to our /sign_in API
endpoint:
1 # snip
2 import axios from 'axios';
3 import deviceStorage from '../services/deviceStorage';
4
5
6 class Login extends Component {
7 constructor(props){
8 super(props);
9 this.state = {
10 email: '',
11 password: '',
12 error: '',
13 loading: false
14 };
15
16 this.loginUser = this.loginUser.bind(this);
17 }
18
19 loginUser() {
20 const { email, password, password_confirmation } = this
21
22 this.setState({ error: '', loading: true });
23
24 // NOTE Post to HTTPS only in production
25 axios.post("http://localhost:4000/api/v1/sign_in",{
26 email: email,
27 password: password
28 })
29 .then((response) => {
30 deviceStorage.saveKey("id_token", response.data.jwt);
31 this.props.newJWT(response.data.jwt);
32 })
33 .catch((error) => {

Similar to Registration.js, we set our form <Button />’s onPress


function to fire the axios.post() that sends credentials from component
state to our API in return for a JWT that we save to device storage and
set to our root component’s state via a props.

POST Error Handler


Let’s add an error handler to both our Registration and Login
components that will run when .catch((error) => {}) is activated.
Create a function in Registration.js called onRegistrationFail() that
sets our error state to ‘Registration Failed’ and loading to false .
Then, bind this onRegistrationFail in the constructor and add it in the
axios .catch():

1 #snip
2
3 class Registration extends Component {
4 constructor(props){
5 # code omitted
6
7 this.registerUser = this.registerUser.bind(this);
8 this.onRegistrationFail = this.onRegistrationFail.bind(
9 }
10
11 registerUser() {
12 # snip
13
14 axios.post("http://localhost:4000/api/v1/sign_up",{
15 # code omitted
16
17 .catch((error) => {
18 console.log(error);
19 this.onRegistrationFail();
20 });

When our error state is set to 'Registration Failed' , the error will be
rendered in the form error text we wrote earlier:

<Text style={errorTextStyle}>
{error}
</Text>

Create and bind a similar onLoginFail() function in Login.js:


1 # snip
2
3 class Login extends Component {
4 constructor(props){
5 # code omitted
6
7 this.loginUser = this.loginUser.bind(this);
8 this.onLoginFail = this.onLoginFail.bind(this);
9 }
10
11 loginUser() {
12 # snip
13
14 axios.post("http://localhost:4000/api/v1/sign_in",{
15 # snip
16 })
17 .catch((error) => {
18 console.log(error);
19 this.onLoginFail();
20 });

Loading and Deleting JWTs with AsyncStorage


Let’s create and export a component containing a “Log Out” button in
screens/LoggedIn.js. This screen will render in App.js when its state
contains a JWT:

1 import React, { Component } from 'react';


2 import { View } from 'react-native';
3 import { Button } from '../components/common/';
4
5 export default class LoggedIn extends Component {
6 constructor(props){
7 super(props);
8 }
9
10 render() {
11 return(
12 <View style={styles.container}>
13 <Button>
14 Log Out
15 </Button>
16 </View>
17 );
Now if we successfully register or log in a user, this screen will render; it
is fairly useless, however, without a function that deletes the app’s JWT
from state and AsyncStorage.

Also, now that we are saving a JWT to device storage on


login/registration, we want to be able to load that JWT from storage
when our app starts up.

Open services/deviceStorage.js, and add another async function


named loadJWT() that checks AsyncStorage for an item with a key of
‘id_token’ via AsyncStorage.getItem(‘id_token’) and sets its value
to state if found:

1 import { AsyncStorage } from 'react-native';


2
3 const deviceStorage = {
4 # code removed for brevity
5
6 async loadJWT() {
7 try {
8 const value = await AsyncStorage.getItem('id_token');
9 if (value !== null) {
10 this.setState({
11 jwt: value,
12 loading: false
13 });
14 } else {
15 this.setState({
16 loading: false

Next, add another async function to deviceStorage called


deleteJWT() that deletes the ‘id_token’ key-value pair in device storage
via AsyncStorage.removeItem(‘id_token’) , then wipes our app’s JWT
state with this.setState({ jwt: ‘’ }) :
1 import { AsyncStorage } from 'react-native';
2
3 const deviceStorage = {
4 # code omitted for brevity
5
6 async deleteJWT() {
7 try{
8 await AsyncStorage.removeItem('id_token')
9 .then(
10 () => {
11 this.setState({
12 jwt: ''
13 })
14 }
15 );

Back in App.js, import deviceStorage at the top of the file. Bind


deviceStorage.deleteJWT and deviceStorage.loadJWT in the
constructor:

1 # snip
2 import deviceStorage from './services/deviceStorage.js';
3
4 export default class App extends Component {
5 constructor() {
6 super();
7 this.state = {
8 jwt: '',
9 }
10
11 this.newJWT = this.newJWT.bind(this);
12 this.deleteJWT = deviceStorage.deleteJWT.bind(this);

By binding our imported deviceStorage helper functions in the


constructor, we enable them to set state. Pretty neat!

Now, add loading: true to our initial state and run this.loadJWT()

at the end of our constructor:


1 # snip
2
3 export default class App extends Component {
4 constructor() {
5 super();
6 this.state = {
7 jwt: '',
8 loading: true
9 }
10
11 this.newJWT = this.newJWT.bind(this);
12 this.deleteJWT = deviceStorage.deleteJWT.bind(this);

this.loadJWT() runs when our App.js component is constructed,


before it is mounted. Running a function in a React component’s
constructor is the same as running a function in the now-deprecated
componentWillMount() component lifecycle method.

Re-write App’s render() function with an initial if (this.state.loading)


statement, then pass this.deleteJWT through <LoggedIn /> as a prop,
as <LoggedIn deleteJWT={this.deleteJWT} /> :

1 # App.js render method


2
3 render() {
4 if (this.state.loading) {
5 return (
6 <Loading size={'large'} />
7 );
8 } else if (!this.state.jwt) {
9 return (
10 <Auth newJWT={this.newJWT} />
11 );
12 } else if (this.state.jwt) {
13 return (

In LoggedIn.js, pass this.props.deleteJWT through our Log Out


button:
1 # snip
2
3 render() {
4 return(
5 <View style={styles.container}>
6 <Button onPress={this.props.deleteJWT}>
7 Log Out
8 </Button>
9 </View>

Now, if you run this app in simulator, the LoggedIn screen will load if
your device contains a JWT!

Also, if you press the Log Out button while logged in, that JWT will be
deleted and you will be sent back to the Auth screen!

Our AsyncStorage-enabled authentication flow is complete!

…but we aren’t making any authenticated requests yet.

Preparing to Make JWT-Authenticated HTTP Requests


We will make a GET request with Authorization: Bearer [jwt here]

in our request headers to our "api/v1/my_user" authenticated API


endpoint.

If successful, this request will respond with the authenticated user’s


database record. We’ll display the email address from the response on
our LoggedIn screen, or an error message if our app request is
unsuccessful.

At the top of LoggedIn.js, import Text from react-native , axios from


axios , and Loading from our common components:

import React, { Component } from 'react';


import { View, Text } from 'react-native';
import { Button, Loading } from '../components/common/';
import axios from 'axios';

Add Loading: true, email: ‘’, error: ‘’ to state in our constructor:


export default class LoggedIn extends Component {
constructor(props){
super(props);
this.state = {
loading: true,
email: '',
error: ''
}
}

In our const styles object, add styles for emailText and


errorText  :

const styles = {
container: {
flex: 1,
justifyContent: 'center'
},
emailText: {
alignSelf: 'center',
color: 'black',
fontSize: 20
},
errorText: {
alignSelf: 'center',
fontSize: 18,
color: 'red'
}
};

Add de-referenced state and styles consts to the top of LoggedIn’s


render():

render() {
const { container, emailText, errorText } = styles;
const { loading, email, error } = this.state;

Now we will add a conditional if/else that will render loading if


this.state.loading = true and our authenticated view if loading =

false  :

if (loading){
return(
<View style={container}>
<Loading size={'large'} />
</View>
)
} else {
return(
<View style={container}>
<View>
{email ?
<Text style={emailText}>
Your email: {email}
</Text>
:
<Text style={errorText}>
{error}
</Text>}
</View>
<Button onPress={this.props.deleteJWT}>
Log Out
</Button>
</View>
);
}

Notice the ternary statement: email ? [email Text] : [error Text] .


If loading: false and email: ‘someEmail@domain.com , the <Text>
component containing {email} will render, otherwise an {error} will
be rendered.

Now, to make the request!

Making JWT-Authenticated HTTP Requests with Axios


in React Native
Before we make our authenticated GET request, we need to make our
root component’s JWT state available to LoggedIn.

Open up App.js and run this.state.jwt through <LoggedIn /> as a


prop, as:

render() {
if (this.state.loading) {
# code omitted
} else if (!this.state.jwt) {
# code omitted
} else if (this.state.jwt) {
return (
<LoggedIn jwt={this.state.jwt} deleteJWT=
{this.deleteJWT} />
);
}
}
Now, go back to LoggedIn.js.

We will make our JWT-authenticated GET request in the


componentDidMount() lifecycle method of our LoggedIn component.

Just as its name implies, the componentDidMount lifecycle method


and the functions it contains run after our component mounts, or after
our component’s constructor() runs and its initial state renders.

First, let’s create our request’s authorization header as a const:

componentDidMount(){
const headers = {
'Authorization': 'Bearer ' + this.props.jwt
};
}

Next, write out a full axios({request_object}) HTTP request:

componentDidMount(){
const headers = {
'Authorization': 'Bearer ' + this.props.jwt
};
axios({
method: 'GET',
url: 'http://localhost:4000/api/v1/my_user',
headers: headers,
})
}

We pass the request type ( GET , if you didn’t know by now) through
method: , our API endpoint through url: , and our JWT
‘Authorization’ headers const through headers: .

Finally, add .then() and .catch() clauses that set email state from


response.data in the event of a successful request, error state in the
event of a failed request, and loading: false in any case:

componentDidMount(){
const headers = {
'Authorization': 'Bearer ' + this.props.jwt
};
axios({
method: 'GET',
url: 'http://localhost:4000/api/v1/my_user',
headers: headers,
}).then((response) => {
this.setState({
email: response.data.email,
loading: false
});
}).catch((error) => {
this.setState({
error: 'Error retrieving data',
loading: false
});
});
}

Our completed LoggedIn.js screen should look like this:


1 import React, { Component } from 'react';
2 import { View, Text } from 'react-native';
3 import { Button, Loading } from '../components/common/';
4 import axios from 'axios';
5
6 export default class LoggedIn extends Component {
7 constructor(props){
8 super(props);
9 this.state = {
10 loading: true,
11 email: '',
12 error: ''
13 }
14 }
15
16 componentDidMount(){
17 const headers = {
18 'Authorization': 'Bearer ' + this.props.jwt
19 };
20 axios({
21 method: 'GET',
22 url: 'http://localhost:4000/api/v1/my_user',
23 headers: headers,
24 }).then((response) => {
25 this.setState({
26 email: response.data.email,
27 loading: false
28 });
29 }).catch((error) => {
30 this.setState({
31 error: 'Error retrieving data',
32 loading: false
33 });
34 });
35 }
36
37 render() {
38 const { container, emailText, errorText } = styles;
39 const { loading, email, error } = this.state;
40
41 if (loading){
42 return(
43 <View style={container}>
44 <Loading size={'large'} />
45 </Vi >
45 </View>
46 )
47 } else {
48 return(
49 <View style={container}>
50 <View>
51 {email ?
52 <Text style={emailText}>
53 Your email: {email}

Beautiful! Shakespearian, almost.

Now, let’s run our Phoenix server from Part I with mix phx.server .

In a new CLI window, run our React Native app with react-native

run-ios and register or log in:


We did it!

Wrapping Up
Congratulations if you made it this far! I hope you got as much out of
reading this guide as I did out of writing it.

Check out the code on Github for reference:


Phoenix 1.4/Elixir API on Github

React Native JWT Client on Github

Please comment below if you have any feedback/questions/issues.

And, as always…

🍹Tips Appreciated! 😉
My Bitcoin address: 1QJuBzHpis4jqQXnSuYxKzGS4Yu3GHhNtX

Bonus: A Note About Asynchronous vs Synchronous


Functions
Async stands for asynchronous; since JavaScript is a single-threaded,
synchronous language, any functions written alongside each other
may run at the same time, creating race conditions if not handled
properly.

One example of a synchronicity problem:

var retrievedValue = AsyncStorage.getItem("some_key");


console.log(retrievedValue);

In almost every case, the console.log() above will fail to display an item
retrieved from AsyncStorage because the console.log() will run
before getItem(“some_key”) finishes running.

Asynchronicity can be achieved through nested callbacks, promises, or


the syntactically delicious async try/await function ( async

funcName(args){ try{ await func(args)} } ).

You might also like