Building a React Native JWT Client: API Requests and AsyncStorage

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 certain 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:

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:

Now, add the following function with a .then() promise to handle JWT responses, and a .catch() function to handle errors:

As you can see above, our Registration’s function POSTs a 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:

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.

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:

Before we do any saving, let’s inspect the response to our user registration POST in console:

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

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

In deviceStorage.saveKey(“id_token”,, 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:

Still in App.js, pass newJWT(JWT) through <Auth> as a prop:

In Auth.js, pass newJWT={this.props.newJWT} as a prop into both Registration and Login within the whichForm() function:

Finally, back in Registration.js, run this.props.newJWT( in our axios callback:

Now, our 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 POSTs our user login email/password state to our /sign_in API endpoint:

Similar to Registration.js, we set our form <Button />’s onPress function to fire the 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():

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

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

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:

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:

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: ‘’ }):

Back in App.js, import deviceStorage at the top of the file. Bind deviceStorage.deleteJWT and deviceStorage.loadJWT in the constructor:

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:

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} />:

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

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:

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

In our const styles object, add styles for emailText and errorText :

Add de-referenced state and styles consts to the top of LoggedIn’s render():

Now we will add a conditional if/else that will render loading if this.state.loading = true and our authenticated view if loading = false :

Notice the ternary statement: email ? [email Text] : [error Text]. If loading: false and email: ‘, 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:

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:

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

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 in the event of a successful request, error state in the event of a failed request, and loading: false in any case:

Our completed LoggedIn.js screen should look like this:

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:

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)} } ).

Nick West (眨眼龙)

Written by | Distributed Systems Engineer

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade