Getting started with React Native & Django authentication - Part 2

May 15, 2018
react-native django auth development

In part 1 we left off with a working authentication API in Django, we will resume here with the creation of our React Native application.

2. Initialising the React Native app

2.1 Required toolset

You will need to have node and npm installed. Navigate to the django-rn-auth directory set up in part 1 and initlialise a new React Native app as follows:

npm install -g create-react-native-app
create-react-native-app authapp && cd authapp

Once the application directory has been initialised, you can start the development server by running:

npm start

This will display a QR code on the terminal, you will need to download the Expo application on your phone and scan the QR code to see you application (both your phone and laptop need to be connected to same network).

2.2 Application layout

We will begin by initialising a authapp/src directory and subdirectories that will contain all our application code and components.

mkdir -p ./src/components

Our application will consist of three screens:

  • 1 - Login screen
  • 2 - Registration screen
  • 3 - Home screen (will serve as the logout screen)

For this we will be using the react-native-router-flux package, based on react navigation.

npm install --save react-native-router-flux

In order to issue the network requests, React Native provides access to the Fetch API out of the box, instead we will be using the axios package as it has some neater features out of the box, such as automatic transformation of JSON data.

npm install --save axios

Ok, we’re ready to start writing some React Native at this point.

2.2.1 Routing

We will begin by defining how the user will navigate through the application. Create a Router.js file inside the .src/ directory:

// src/Router.js 

import React from 'react';
import { Scene, Stack, Router, Actions } from 'react-native-router-flux';
import Login from './components/Login';
import Register from './components/Register';
import Home from './components/Home';


const RouterComponent = () => {
  return (
    <Router>
      <Stack hideNavBar key="root">
        <Stack
          key="auth"
          type="reset"
        >
          <Scene
            title="Sign In"
            key="login"
            component={Login}
            initial
          />
          <Scene
            title="Register"
            key="register"
            component={Register}
          />  
        </Stack>
        <Stack
          key="main"
          type="reset"
        >
          <Scene
            title="Home"
            key="home"
            component={Home}
            initial
          />
        </Stack>
      </Stack>
    </Router>
  );
};


export default RouterComponent;

If you have issues properly displaying the title and/or location of the navigation bar, you will need to do some styling on the components.

Add the required imports:

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

Add the style properties:

const style = StyleSheet.create({
  navBarStyle: {
    top: StatusBar.currentHeight
  },
  titleStyle: {
    flexDirection: 'row',
    width: 200
  }
});

Modify the auth and main Stacks:

<Stack ...>
    <Stack
      key="auth"
      ...
      navigationBarStyle={style.navBarStyle}
      titleStyle={style.titleStyle}
    >
    <Stack
      key="main"
      ...
      navigationBarStyle={style.navBarStyle}
      titleStyle={style.titleStyle}
    >
</Stack>

2.2.2 Screen components

Let’s now add the three components that will embody our three main screens. These will for now be dummy components to get something going. Create the following inside the src/components directory:

  • Login
// src/components/Login.js

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


class Login extends Component {
  render() {
    return (
      <View>
        <Text>Login screen</Text>
      </View>
    );
  }
}

export default Login;
  • Registration
// src/components/Register.js

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


class Register extends Component {
  render() {
    return (
      <View>
        <Text>Registration screen</Text>
      </View>
    );
  }
}

export default Register;
  • Home
// src/components/Home.js

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


class Home extends Component {
  render() {
    return (
      <View>
        <Text>Home screen</Text>
      </View>
    );
  }
}

export default Home;

One last thing we will need to do in order to get this working is replacing the contents of the default App.js in the root of our project. We need to indicate that the router component will be handling what is being displayed:

// App.js

import React, { Component } from 'react';
import Router from './src/Router';


export default class App extends Component {
  render() {
    return (
      <Router />
    );
  }
}

This should be enough the application to a working state with routing.

2.3 Defining the main components

2.3.1 Login and register components

NOTE: This is the ideal scenario for a redux implementation in order to share the state between components. For the sake of simplifying this example we will not be using it.

There are a number of similitudes between these two components. Both will get some input data, maybe do some processing with it and issue a POST request to our API register or login endpoints. It seems sensible then to create a common component that can be used in both the Login and Register components. We will achieve this by passing a create prop into the component that will add a couple of extra fields and change the behaviour and look of the submit button.

Create a subdirectory src/components/common/ and touch a file called LoginOrCreateForm.js. Our common component will look like this:

 // src/components/common/LoginOrCreateForm.js

import React, { Component } from 'react';
import { Button, View, Text, TextInput, StyleSheet } from 'react-native';
import { Actions } from 'react-native-router-flux';
import axios from 'axios';


class LoginOrCreateForm extends Component {
  state = {
    username: '',
    password: '',
    firstName: '',
    lastName: ''
  }

  onUsernameChange(text) {
    this.setState({ username: text });
  }

  onPasswordChange(text) {
    this.setState({ password: text });
  }

  onFirstNameChange(text) {
    this.setState({ firstName: text });
  }

  onLastNameChange(text) {
    this.setState({ lastName: text });
  }

  renderCreateForm() {
    const { fieldStyle, textInputStyle } = style;
    if (this.props.create) {
      return (
          <View style={fieldStyle}>
            <TextInput
              placeholder="First name"
              autoCorrect={false}
              onChangeText={this.onFirstNameChange.bind(this)}
              style={textInputStyle}
            />
            <TextInput
              placeholder="Last name"
              autoCorrect={false}
              onChangeText={this.onLastNameChange.bind(this)}
              style={textInputStyle}
            />
          </View>
      );
    }
  }

  renderButton() {
    const buttonText = this.props.create ? 'Create' : 'Login';

    return (
      <Button title={buttonText} onPress={this.handleRequest.bind(this)}/>
    );
  }


  renderCreateLink() {
    if (!this.props.create) {
      const { accountCreateTextStyle } = style;
      return (
        <Text style={accountCreateTextStyle}>
          Or 
          <Text style={{ color: 'blue' }} onPress={() => Actions.register()}>
            {' Sign-up'}
          </Text>
        </Text>
      );
    }
  }

  render() {
    const {
      formContainerStyle,
      fieldStyle,
      textInputStyle,
      buttonContainerStyle,
      accountCreateContainerStyle
    } = style;

    return (
      <View style={{ flex: 1, backgroundColor: 'white' }}>
        <View style={formContainerStyle}>
          <View style={fieldStyle}>
            <TextInput
              placeholder="username"
              autoCorrect={false}
              autoCapitalize="none"
              onChangeText={this.onUsernameChange.bind(this)}
              style={textInputStyle}
            />
          </View>
          <View style={fieldStyle}>
            <TextInput
              secureTextEntry
              autoCapitalize="none"
              autoCorrect={false}
              placeholder="password"
              onChangeText={this.onPasswordChange.bind(this)}
              style={textInputStyle}
            />
          </View>
          {this.renderCreateForm()}
        </View>
        <View style={buttonContainerStyle}>
          {this.renderButton()}
          <View style={accountCreateContainerStyle}>
            {this.renderCreateLink()}
          </View>
        </View>
      </View>
    );
  }
}


const style = StyleSheet.create({
  formContainerStyle: {
    flex: 1,
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
  },
  textInputStyle: {
    flex: 1,
    padding: 15
  },
  fieldStyle: {
    flexDirection: 'row',
    justifyContent: 'center'
  },
  buttonContainerStyle: {
    flex: 1,
    justifyContent: 'center',
    padding: 25
  },
  accountCreateTextStyle: {
    color: 'black'
  },
  accountCreateContainerStyle: {
    padding: 25,
    alignItems: 'center'
  }
});


export default LoginOrCreateForm;

We can now replace the contents of our Login and Register components as so:

// src/components/Login.js

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import LoginOrCreateForm from './common/LoginOrCreateForm';


class Login extends Component {
  render() {
    return (
      <View style={{ flex: 1 }}>
        <LoginOrCreateForm />
      </View>
    );
  }
}

export default Login;
// src/components/Register.js

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import LoginOrCreateForm from './common/LoginOrCreateForm';

class Register extends Component {
  render() {
    return (
      <View style={{ flex: 1 }}>
        <LoginOrCreateForm create/>
      </View>
    );
  }
}

export default Register;

As you can see, when the button is pressed nothing happens. Here is where we get to the meat of this example. We will add an onPress handler that will post the requested data to our backend endpoint and redirect to the Home screen if the response is successfull.

Add the following method to the LoginOrCreateForm class:

handleRequest() {
  const endpoint = this.props.create ? 'register' : 'login';
  const payload = { username: this.state.username, password: this.state.password } 
  
  if (this.props.create) {
    payload.first_name = this.state.firstName;
    payload.last_name = this.state.lastName;
  }
  
  axios
    .post(`/auth/${endpoint}/`, payload)
    .then(response => {
      const { token, user } = response.data;

      // We set the returned token as the default authorization header
      axios.defaults.headers.common.Authorization = `Token ${token}`;
      
      // Navigate to the home screen
      Actions.main();
    })
    .catch(error => console.log(error));
}

And hook it up with the button’s onPress handler:

renderButton() {
    const buttonText = this.props.create ? 'Create' : 'Login';

    return (
      <Button title={buttonText} onPress={this.handleRequest.bind(this)}/>
    );
  }

Although it can be done, note that we are not passing the entire URL in the request. Axios has a few tricks to help us manage its configuration.

We can set them as defaults upon application load. Navigate to the App.js file in the root directory and add the following method to the App class:

componentWillMount() {
    axios.defaults.baseURL = 'http://<YOUR_LOCAL_IP>:8000/api';
    axios.defaults.timeout = 1500;
  }

Remember to replace <YOUR_LOCAL_IP> with your actual local IP address, and if for some reason you’re running the django server on a different port, change this as well.

At this point you should be able to start your backend Django server with your local IP and login via the app. Navigate to the backend directory set up in part 1 and start the server as follows (remember to activate your virtualenv):

python manage.py runserver <YOUR_LOCAL_IP>:8000

And replace <YOUR_LOCAL_IP> for your local IP.

You should be able to login or register and access the home screen.

2.3.2 Logout component

The logout component will simply consist of a button with a callback to our logout endpoint, upon successful response, the user will be redirected to the login screen:

import React, { Component } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import { Actions } from 'react-native-router-flux';
import axios from 'axios';


class Home extends Component {

  handleRequest() {
    // This request will only succeed if the Authorization header
    // contains the API token
    axios
      .get('/auth/logout/')
      .then(response => {
        Actions.auth()
      })
      .catch(error =>  console.log(error));
  }

  render() {
    const { buttonContainerStyle } = styles;
    return (
      <View style={buttonContainerStyle}>
        <Button title="Logout" onPress={this.handleRequest.bind(this)}/>
      </View>
    );
  }
}


const styles = StyleSheet.create({
  buttonContainerStyle: {
    flex: 1,
    flexDirection: 'column',
    justifyContent: 'center',
    backgroundColor: 'white'
  }
});

export default Home;

And voilà!

NOTE: This is a very simplified version of what authentication with React Native and Django looks like. We have opted for a simple yet comprehensive approach and have compromised on multiple aspects that SHOULD be taken into consideration when creating a production ready application, find a short list of these as follows:

  • Field validation
  • Application state management
  • Error handling
  • Response status code handling
  • Endpoint security
  • Throttling

All the code for this project can be found in this repo

comments powered by Disqus