React Native Login API Example with Axios: A Step‑by‑Step Guide
Building a secure login flow is one of the first challenges developers face when creating a mobile app with React Native. Even so, by combining React Native’s component model with Axios—a promise‑based HTTP client—you can easily communicate with a backend REST API, handle authentication tokens, and manage user state. Even so, this article walks you through a complete, production‑ready example that shows how to set up a React Native project, install Axios, create a reusable API service, design a login screen, process the response, store the token securely, and work through to the protected part of your app. Follow each section carefully, and you’ll have a working login implementation that you can adapt to any API specification.
1. Setting Up the React Native Project
Before writing any code, ensure you have the React Native CLI (or Expo) installed and a working development environment for Android/iOS.
# Using React Native CLI
npx react-native init LoginAppDemo
cd LoginAppDemo
# If you prefer Expo
# expo init LoginAppDemo
# cd LoginAppDemo
Once the project is created, run it on an emulator or device to verify the baseline works:
npx react-native run-android # or run-ios
You should see the default welcome screen. From here we’ll add the dependencies needed for API communication and state management.
2. Installing Required Dependencies
The core library for making HTTP requests is Axios. We’ll also add @react-native-async-storage/async-storage to persist the JWT (or any token) returned by the login endpoint, and @react-navigation/native with its stack navigator to handle screen transitions after a successful login No workaround needed..
npm install axios @react-native-async-storage/async-storage @react-navigation/native @react-navigation/stack
# For Expo users, replace npm with expo install where needed
If you are using the bare React Native CLI, you may need to link native modules (though recent versions autolink). For AsyncStorage on iOS, run:
npx pod-install
3. Creating a Reusable Axios Service
Instead of scattering axios.Consider this: get or axios. Practically speaking, post calls throughout your components, centralize the configuration in a service file. This makes it easy to change the base URL, add headers, or implement interceptors for token refresh logic later.
Create a folder src/services/ and add api.js:
// src/services/api.js
import axios from 'axios';
// Replace with your actual backend URL
const BASE_URL = 'https://api.example.com';
const api = axios.create({
baseURL: BASE_URL,
timeout: 10000, // 10 seconds
headers: {
'Content-Type': 'application/json',
// Accept header can be adjusted based on your API
Accept: 'application/json',
},
});
// Optional: request interceptor to attach auth token
api.In practice, getItem('userToken');
if (token) {
config. request.Even so, interceptors. headers.use(
async (config) => {
const token = await require('@react-native-async-storage/async-storage')
.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.
export default api;
Key points:
- The
baseURLensures all endpoints are relative to your backend. - A timeout prevents hanging requests.
- The request interceptor automatically injects a stored JWT into the
Authorizationheader for every outgoing call—handy for protected routes later.
4. Designing the Login Screen
We’ll build a simple functional component using React hooks. The screen contains two TextInput fields (email and password), a button to submit, and basic loading/error states Worth keeping that in mind..
Create src/screens/LoginScreen.js:
// src/screens/LoginScreen.js
import React, { useState } from 'react';
import {
View,
TextInput,
Button,
Text,
ActivityIndicator,
StyleSheet,
Alert,
} from 'react-native';
import api from '../services/api';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useNavigation } from '@react-navigation/native';
export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const navigation = useNavigation();
const handleLogin = async () => {
if (!That's why email. Because of that, trim() || ! Here's the thing — password. trim()) {
Alert.alert('Validation Error', 'Please fill in both fields.
setLoading(true);
try {
const response = await api.post('/auth/login', {
email: email.trim(),
password,
});
// Assuming the API returns { token: 'jwt-string', user: { ... } }
const { token, user } = response.data;
// Store token securely
await AsyncStorage.setItem('userToken', token);
// Optionally store user info
await AsyncStorage.setItem('userInfo', JSON.
// figure out to home or dashboard
navigation.replace('Home');
} catch (err) {
let message = 'Login failed. Please try again.';
if (err.response && err.Practically speaking, response. data && err.response.In practice, data. message) {
message = err.response.data.message;
}
Alert.
return (
Login to Your App
{loading ? (
) : (
)}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingHorizontal: 24,
backgroundColor: '#fafafa',
},
title: {
fontSize: 24,
fontWeight: '600',
marginBottom: 30,
textAlign: 'center',
color: '#333',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 6,
paddingHorizontal: 12,
paddingVertical: 10,
marginBottom: 16,
backgroundColor: '#fff',
},
});
Explanation of important parts:
- State management:
useStatetracks email, password
Enhancing the Login Experience
The snippet you saw gives you a functional login screen, but most production‑grade applications demand a few extra layers of polish and protection. Below are several practical enhancements you can apply to the existing component without rewriting the whole file Simple as that..
1. dependable Input Validation
While the current check prevents empty fields, a login form should also verify that the email address follows a proper format and that the password meets any required strength criteria. Leveraging a lightweight validation library such as Yup or zod keeps the logic declarative and testable:
// Example validation schema (not part of the component)
const loginSchema = Yup.object().shape({
email: Yup.string()
.email('Invalid email address')
.required('Email is required'),
password: Yup.string()
.min(8, 'Password must be at least 8 characters')
.required('Password is required'),
});
You can integrate this schema into handleLogin by running loginSchema.validate({ email, password }, { abortEarly: false }). If validation fails, you can surface each error message next to its respective input, giving users immediate feedback rather than a generic alert.
2. Secure Token Storage
AsyncStorage is convenient, but it stores data in plain text on the device. For apps that handle sensitive authentication tokens, consider using react‑native‑keychain or expo‑secure‑store. These libraries encrypt the stored value and can enforce biometric prompts (Face ID, Touch ID) on iOS and biometric authentication on Android.
Most guides skip this. Don't.
import * as Keychain from 'react-native-keychain';
const storeToken = async (token) => {
try {
await Keychain.setInternetCredentials('myApp', 'authToken', token);
} catch (error) {
console.error('Failed to store token securely', error);
}
};
When you retrieve the token later, you can also enforce that it’s stored with the proper access restrictions (e., ACCESSIBLE.Even so, g. WHEN_UNLOCKED_THIS_DEVICE_ONLY) Worth knowing..
3. Comprehensive Error Handling
The current catch block extracts a server‑provided message, but network‑level failures (offline mode, 5xx errors, malformed JSON) also need graceful treatment. You can augment the error handling like this:
const handleLogin = async () => {
// validation omitted for brevity
setLoading(true);
try {
const response = await api.post('/auth/login', payload);
// success flow
} catch (err) {
if (!err.response) {
// No response – likely a network issue
Alert.alert('Network Error', 'Please check your internet connection.');
} else {
const message = err.response?.data?.message ?? 'Login failed.';
Alert.alert('Authentication Failed',
message);
}
} finally {
setLoading(false);
}
};
In production, you may prefer inline error messages instead of Alert.alert, especially for repeated login attempts. Inline feedback is easier to style, more accessible, and avoids interrupting the user with modal popups.
4. Prevent Duplicate Submissions
Users may tap the login button multiple times while waiting for a response. Disabling the button while the request is in progress prevents duplicate submissions and reduces unnecessary load on the authentication endpoint.
{loading ? 'Logging in...' : 'Login'}
You can also improve keyboard behavior by setting returnKeyType="done" on the password field and submitting the form when the user presses Enter.
5. Centralize Authentication State
For small apps, keeping the token in the login component may be acceptable. As the app grows, authentication state should usually be managed in a shared context, store, or auth provider. This prevents scattered token checks and makes it easier to redirect users, refresh sessions, and log them out globally.
No fluff here — just what actually works.
Here's one way to look at it: an auth context can expose values such as:
const auth = {
user,
token,
isAuthenticated,
login,
logout,
loading,
};
The login screen then only needs to collect credentials and call auth.login(email, password), while the rest of the app consumes the same authentication state That's the whole idea..
6. Add Basic Tests
A login screen is a critical user flow, so it should be covered by at least a few focused tests. Useful cases include:
- Preventing submission with empty fields
- Showing validation errors for invalid email formats
- Displaying loading state during login
- Handling failed API responses
- Storing the token after a successful login
With React Native Testing Library, a simple test might look like this:
import { render, fireEvent, screen } from '@testing-library/react-native';
import LoginScreen from '../LoginScreen';
test('shows validation error when email is empty', async () => {
render( );
fireEvent.changeText(screen.getByPlaceholderText(/password/i), 'password123');
fireEvent.press(screen.getByText(/login/i));
expect(await screen.findByText(/email is required/i)).toBeTruthy();
});
Testing these basic scenarios helps catch regressions when the login flow changes later.
Conclusion
The existing login implementation provides a solid starting point, but production-ready authentication requires a bit more care. By adding structured validation, secure token storage, stronger error handling, duplicate-submit protection
, and centralized state management, you can build a login flow that is both user-friendly and solid. Because of that, these practices not only improve the immediate user experience but also lay a foundation for scalability and maintainability as your application evolves. Remember that authentication is a critical component of any app, and investing time in these details will pay off in reduced bugs, better security, and easier feature expansion down the road. By following these steps, you see to it that your login system is ready for the complexities of real-world usage.