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
We want backend to deal with validation
We want the layout to work on iPhone 6S to iPhone XS Max
We want to use JSON Web Token after login is successful
We have four cases we need to address
All fields are empty
One field is empty
Both fields have data but do not match the data in the backend
The data provided is correct and JSON Web Token is returned from the backend
The data request / response cycle
[FRONTEND] User enters data in username and password fields
[FRONTEND] User presses the LOGIN button
[FRONTEND] Data from the form is sent to the backend with APIKit
[BACKEND] Data from the form is received and validation check begins
[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
[FRONTEND] Data is received by the frontend
[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
Install node:
brew install node
Install watchman:
brew install watchman
Install XCode from the App Store or Apple's Developer Portal
Instal cocoapods:
sudo gem install cocoapods
Create the project:
npx react-native init ReactNativeExample
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
App.js - The landing view that wraps all other views
LoginView.js - Shows the LoginForm and contains logic for the Login view
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 ourthis.state.username
thruonUsernameChange(...)
Whenever user types something in the
password
field we will update ourthis.state.password
thruonPasswordChange(...)
Whenever user presses login we will fire
onPressLogin(...)
When data from backend arrives we will have to either call
onSuccess
oronFailure
inside ouronPressLogin(...)
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