When I first started wiring up API calls with Redux in my React JS project, I kept running into a 401 Unauthorized error. If you’ve seen that dreaded number, you know how frustrating it can be. A 401 usually means the server didn’t receive a valid authentication header. In my case, I thought I was sending everything correctly: phone number in the body and access token in the header. But it still failed.
After breaking it down, I realized there were a few common pitfalls in my code.
Where I Went Wrong
- Headers object was wrong
I passed my token like this:axios.post(url, body, { Authorization: `Bearer ${userdata.auth}` })
But Axios expects headers to be nested inside aheaders
key:axios.post(url, body, { headers: { Authorization: `Bearer ${token}` } })
- I forgot
await
Without awaiting the promise,response
was just a pending promise andresponse.data
wasundefined
. - Axios instance confusion
I creatednew Axios()
but then calledaxios.post(...)
directly. It’s better to stick with a singleaxios.create()
client. - Body format wasn’t consistent
The API wanted raw JSON. Axios handles JSON automatically if I setContent-Type: application/json
. - Phone number format
Many verification APIs require phone numbers in E.164 format (+<country><number>
). I wasn’t adding the+
prefix.
Clean, Working Project Setup
Here’s how I fixed my Redux project step by step.
Axios Client
// api/client.js
import axios from "axios";
export const api = axios.create({
baseURL: "https://theappsouk.com/api",
headers: {
"Content-Type": "application/json",
},
});
Action Types
// actions/types.js
export const VERIFY_PHONE_NUMBER_REQUEST = "VERIFY_PHONE_NUMBER_REQUEST";
export const VERIFY_PHONE_NUMBER_SUCCESS = "VERIFY_PHONE_NUMBER_SUCCESS";
export const VERIFY_PHONE_NUMBER_FAILURE = "VERIFY_PHONE_NUMBER_FAILURE";
Action Creator
// actions/auth.js
import { api } from "../api/client";
import {
VERIFY_PHONE_NUMBER_REQUEST,
VERIFY_PHONE_NUMBER_SUCCESS,
VERIFY_PHONE_NUMBER_FAILURE,
} from "./types";
export const verifyPhoneNumber = ({ pnumber, auth }) => {
return async (dispatch) => {
dispatch({ type: VERIFY_PHONE_NUMBER_REQUEST });
try {
// Ensure + prefix for E.164 format
const phone_number = pnumber.startsWith("+") ? pnumber : `+${pnumber}`;
const response = await api.post(
"/Profile/SendVerificationSmsCode",
{ phone_number },
{
headers: {
Authorization: `Bearer ${auth}`,
},
}
);
const { data } = response;
dispatch({ type: VERIFY_PHONE_NUMBER_SUCCESS, payload: data });
return data;
} catch (err) {
const status = err?.response?.status;
const message =
status === 401
? "Unauthorized: your session may have expired or the token is invalid."
: err?.response?.data?.message || err.message || "Request failed";
dispatch({
type: VERIFY_PHONE_NUMBER_FAILURE,
error: { status, message },
});
}
};
};
Reducer
// reducers/auth.js
import {
VERIFY_PHONE_NUMBER_REQUEST,
VERIFY_PHONE_NUMBER_SUCCESS,
VERIFY_PHONE_NUMBER_FAILURE,
} from "../actions/types";
const initialState = {
verifyPhoneNumber: null,
loading: false,
error: null,
};
export default function authReducer(state = initialState, action) {
switch (action.type) {
case VERIFY_PHONE_NUMBER_REQUEST:
return { ...state, loading: true, error: null };
case VERIFY_PHONE_NUMBER_SUCCESS:
return {
...state,
loading: false,
verifyPhoneNumber: action.payload,
error: null,
};
case VERIFY_PHONE_NUMBER_FAILURE:
return { ...state, loading: false, error: action.error };
default:
return state;
}
}
React Component with Extra Functionality
I also added:
- Client-side validation
- Loading feedback
- Error messages
- A “resend code” button with a 30-second cooldown
// Comp.jsx
import React, { Component } from "react";
import { connect } from "react-redux";
import { verifyPhoneNumber } from "../actions/auth";
class Comp extends Component {
constructor(props) {
super(props);
this.state = {
number: "",
error: "",
cooldown: 0,
timerId: null,
};
}
componentWillUnmount() {
if (this.state.timerId) clearInterval(this.state.timerId);
}
startCooldown = (seconds = 30) => {
if (this.state.timerId) clearInterval(this.state.timerId);
this.setState({ cooldown: seconds });
const timerId = setInterval(() => {
this.setState((s) => {
if (s.cooldown <= 1) {
clearInterval(timerId);
return { cooldown: 0, timerId: null };
}
return { cooldown: s.cooldown - 1 };
});
}, 1000);
this.setState({ timerId });
};
handleChange = (e) => this.setState({ number: e.target.value, error: "" });
submitPhoneNumber = async () => {
const { number } = this.state;
const code = this.props.user?.data?.user?.country_code || "";
const token = this.props.user?.data?.user?.access_token || "";
if (!number) {
this.setState({ error: "Phone Number is required" });
return;
}
if (!/^\d{6,15}$/.test(number)) {
this.setState({ error: "Enter a valid phone number (6–15 digits)" });
return;
}
if (!token) {
this.setState({ error: "Missing access token. Please log in again." });
return;
}
const pnumber = `${code}${number}`;
const result = await this.props.verifyPhoneNumber({ pnumber, auth: token });
if (result?.status) {
this.startCooldown(30);
}
};
render() {
const { loading, error: serverError } = this.props.auth || {};
const { number, error, cooldown } = this.state;
const countryCode = this.props.user?.data?.user?.country_code || "DK";
return (
<form className="form bg-white p-3 mb-3">
<div className="row">
<div className="col-4 mr-0">
<label>Country code</label>
<select className="custom-select" disabled>
<option>{countryCode}</option>
</select>
</div>
<div className="col-8 ml-0 pl-0">
<label>Mobile number</label>
<input
type="text"
className="form-control"
placeholder="Mobilnummer"
value={number}
onChange={this.handleChange}
disabled={loading}
/>
</div>
</div>
<small className="text-danger my-0 py-0">
{error || serverError?.message}
</small>
<div className="form-group mb-0 text-right mt-3">
<button
className="btn btn-default"
type="button"
onClick={this.submitPhoneNumber}
disabled={loading || cooldown > 0}
title={cooldown > 0 ? `Resend available in ${cooldown}s` : ""}
>
{loading
? "Sending..."
: cooldown > 0
? `Resend in ${cooldown}s`
: "Send Code"}
</button>
</div>
</form>
);
}
}
const mapState = (state) => ({
user: state.user,
auth: state.auth,
});
export default connect(mapState, { verifyPhoneNumber })(Comp);
Final Thought
For me, the 401 error wasn’t really about authorization it was about the way I was structuring my Axios call. By fixing the headers, awaiting properly, and validating the phone format, the issue disappeared. I also learned that adding small “practice features” like retries, cooldowns, and validation made my app more user-friendly and reliable. If you’re struggling with a 401 error in React + Redux, double check your headers and response handling first it’s usually something small but critical.