Build a great login experience with React Native, Axios and JSONWebToken

2019-12-17

Mobile on purple background

Most apps need some kind of login in order to serve data that is related the to the authorized User. In this short tutorial we are going to build a good looking login form in React Native.

The code for this example has been published at Github

NOTE: We are relying on a Django REST Framework backend in this example. It is OK if You are using a different backend framework. You just have to make the necessary adjustments in order to make sure that Your React Native app is compatible with Your backend.

Grab a coffee and let's get started!

The big picture

Goals to achieve

  1. We want backend to deal with validation

  2. We want the layout to work on iPhone 6S to iPhone XS Max

  3. We want to use JSON Web Token after login is successful

We have four cases we need to address

  1. All fields are empty

  2. One field is empty

  3. Both fields have data but do not match the data in the backend

  4. The data provided is correct and JSON Web Token is returned from the backend

The data request / response cycle

  1. [FRONTEND] User enters data in username and password fields

  2. [FRONTEND] User presses the LOGIN button

  3. [FRONTEND] Data from the form is sent to the backend with APIKit

  4. [BACKEND] Data from the form is received and validation check begins

  5. [BACKEND] If data is valid a JSON Web Token is returned with HTTP 200 and if data is NOT valid a JSON payload is returned containing error messages

  6. [FRONTEND] Data is received by the frontend

  7. [FRONTEND] If frontend gets a HTTP 200 it will execute onSuccess or if it receives HTTP 4xx/5xx it will execute onFailure

Javascript// onSuccess in `onPressLogin` method in `LoginView.js`
const onSuccess = ({data}) => {
  // Set JSON Web Token on success
  setClientToken(data.token);
  this.setState({isLoading: false, isAuthorized: true});
};

// onFailure in `onPressLogin` method in `LoginView.js`
const onFailure = error => {
  console.warn(error && error.response);
  this.setState({errors: error.response.data, isLoading: false});
};

1. Install React Native and its dependencies

  1. Install node: brew install node

  2. Install watchman: brew install watchman

  3. Install XCode from the App Store or Apple's Developer Portal

  4. Instal cocoapods: sudo gem install cocoapods

  5. Create the project: npx react-native init ReactNativeExample

  6. Change folder and kickstart the simulator cd ReactNativeExample && npx react-native run-ios

NOTE: The setup above shows instructions for MacOS, if You are using Linux or Windows please have a look at the official docs here

2. Replace Your App.js in the root folder

We will basically create our logic in three files

  1. App.js - The landing view that wraps all other views

  2. LoginView.js - Shows the LoginForm and contains logic for the Login view

  3. APIKit.js - Contains logic related to the Axios client and JSON Web Token

NOTE: We will not implement routing in this project

Drop the code snippet below and replace all the code You have in Your App.js file

Javascript// <ROOT>/App.js

import React from 'react';

import {
  SafeAreaView,
  StyleSheet,
  KeyboardAvoidingView,
  StatusBar,
} from 'react-native';

import LoginView from './App/Views/Login/LoginView';

const App = () => {
  return (
    <KeyboardAvoidingView
      behavior={Platform.OS === 'ios' ? 'padding' : 'position'}
      style={{flex: 1}}
      enabled>
      <StatusBar barStyle="dark-content" />

      <SafeAreaView style={styles.container}>
        <LoginView />
      </SafeAreaView>
    </KeyboardAvoidingView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f6f6f6"
  },
});

export default App;

3. Add the APIKit.js to handle HTTP calls to the backend

We will use axios here, to install it via yarn add axios or npm install axios --save

Javascript//<ROOT>/shared/APIKit.js
import axios from 'axios';

// Create axios client, pre-configured with baseURL
let APIKit = axios.create({
  baseURL: 'https://app.example.se',
  timeout: 10000,
});

// Set JSON Web Token in Client to be included in all calls
export const setClientToken = token => {
  APIKit.interceptors.request.use(function(config) {
    config.headers.Authorization = `Bearer ${token}`;
    return config;
  });
};

export default APIKit;

4. Add the LoginView.js

Create the LoginView.js file under App/Views/Login/LoginView.js

We will use a spinner library for React Native, install it by running yarn add react-native-loading-spinner-overlay or npm install react-native-loading-spinner-overlay --save

NOTE: We are fully relying on backend to handle validation for us. So we do not validate the data in our App

NOTE: You will have to create the App folder in the project's root folder

Our state for this view will be handled locally. Please have a look at the snippet below.

Javascriptconst initialState = {
  username: '',        // Store `username` when user enters their username
  password: '',        // Store `password` when user enters their password
  errors: {},          // Store error data from the backend here
  isAuthorized: false, // If auth is successful, set this to `true`
  isLoading: false,    // Set this to `true` if You want to show spinner
};

Things to bear in mind

  • Whenever user types something in the username field we will update our this.state.username thru onUsernameChange(...)

  • Whenever user types something in the password field we will update our this.state.password thru onPasswordChange(...)

  • Whenever user presses login we will fire onPressLogin(...)

  • When data from backend arrives we will have to either call onSuccess or onFailure inside our onPressLogin(...) method

Javascript// <ROOT>/App/Views/Login/LoginView.js

import React, {Component} from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  Image,
  TextInput
} from 'react-native';

import Spinner from 'react-native-loading-spinner-overlay';

import APIKit, {setClientToken} from '../../../shared/APIKit';

const initialState = {
  username: '',
  password: '',
  errors: {},
  isAuthorized: false,
  isLoading: false,
};

class Login extends Component {
  state = initialState;

  componentWillUnmount() {}

  onUsernameChange = username => {
    this.setState({username});
  };

  onPasswordChange = password => {
    this.setState({password});
  };

  onPressLogin() {
    const {username, password} = this.state;
    const payload = {username, password};
    console.log(payload);

    const onSuccess = ({data}) => {
      // Set JSON Web Token on success
      setClientToken(data.token);
      this.setState({isLoading: false, isAuthorized: true});
    };

    const onFailure = error => {
      console.log(error && error.response);
      this.setState({errors: error.response.data, isLoading: false});
    };

    // Show spinner when call is made
    this.setState({isLoading: true});

    APIKit.post('/api-token-auth/', payload)
      .then(onSuccess)
      .catch(onFailure);
  }

  getNonFieldErrorMessage() {
    // Return errors that are served in `non_field_errors`
    let message = null;
    const {errors} = this.state;
    if (errors.non_field_errors) {
      message = (
        <View style={styles.errorMessageContainerStyle}>
          {errors.non_field_errors.map(item => (
            <Text style={styles.errorMessageTextStyle} key={item}>
              {item}
            </Text>
          ))}
        </View>
      );
    }
    return message;
  }

  getErrorMessageByField(field) {
    // Checks for error message in specified field
    // Shows error message from backend
    let message = null;
    if (this.state.errors[field]) {
      message = (
        <View style={styles.errorMessageContainerStyle}>
          {this.state.errors[field].map(item => (
            <Text style={styles.errorMessageTextStyle} key={item}>
              {item}
            </Text>
          ))}
        </View>
      );
    }
    return message;
  }

  render() {
    const {isLoading} = this.state;

    return (
      <View style={styles.containerStyle}>
        <Spinner visible={isLoading} />

        {!this.state.isAuthorized ? <View>
          <View style={styles.logotypeContainer}>
            <Image
              source={require('../../../assets/images/logo/touchbase-logo.png')}
              style={styles.logotype}
            />
          </View>

          <TextInput
            style={styles.input}
            value={this.state.username}
            maxLength={256}
            placeholder="Enter username..."
            autoCapitalize="none"
            autoCorrect={false}
            returnKeyType="next"
            onSubmitEditing={event =>
              this.passwordInput.wrappedInstance.focus()
            }
            onChangeText={this.onUsernameChange}
            underlineColorAndroid="transparent"
            placeholderTextColor="#999"
          />

          {this.getErrorMessageByField('username')}

          <TextInput
            ref={node => {
              this.passwordInput = node;
            }}
            style={styles.input}
            value={this.state.password}
            maxLength={40}
            placeholder="Enter password..."
            onChangeText={this.onPasswordChange}
            autoCapitalize="none"
            autoCorrect={false}
            returnKeyType="done"
            blurOnSubmit
            onSubmitEditing={this.onPressLogin.bind(this)}
            secureTextEntry
            underlineColorAndroid="transparent"
            placeholderTextColor="#999"
          />

          {this.getErrorMessageByField('password')}

          {this.getNonFieldErrorMessage()}

          <TouchableOpacity
            style={styles.loginButton}
            onPress={this.onPressLogin.bind(this)}>
            <Text style={styles.loginButtonText}>LOGIN</Text>
          </TouchableOpacity>
        </View> : <View><Text>Successfully authorized!</Text></View>}
      </View>
    );
  }
}

// Define some colors and default sane values
const utils = {
  colors: {primaryColor: '#af0e66'},
  dimensions: {defaultPadding: 12},
  fonts: {largeFontSize: 18, mediumFontSize: 16, smallFontSize: 12},
};

// Define styles here
const styles = {
  innerContainer: {
    marginBottom: 32,
  },
  logotypeContainer: {
    alignItems: 'center',
  },
  logotype: {
    maxWidth: 280,
    maxHeight: 100,
    resizeMode: 'contain',
    alignItems: 'center',
  },
  containerStyle: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#f6f6f6',
  },
  input: {
    height: 50,
    padding: 12,
    backgroundColor: 'white',
    borderRadius: 6,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 4,
    },
    shadowOpacity: 0.05,
    shadowRadius: 4,
    marginBottom: utils.dimensions.defaultPadding,
  },
  loginButton: {
    borderColor: utils.colors.primaryColor,
    borderWidth: 2,
    padding: utils.dimensions.defaultPadding,
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 6,
  },
  loginButtonText: {
    color: utils.colors.primaryColor,
    fontSize: utils.fonts.mediumFontSize,
    fontWeight: 'bold',
  },
  errorMessageContainerStyle: {
    marginBottom: 8,
    backgroundColor: '#fee8e6',
    padding: 8,
    borderRadius: 4,
  },
  errorMessageTextStyle: {
    color: '#db2828',
    textAlign: 'center',
    fontSize: 12,
  },
};

export default Login;

That's it. You now have a pretty solid base to work with. Just add some navigation library and You are good to go. Read more about Navigation here