How to Resolve 401 Error in React JS Using Redux?

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

  1. 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 a headers key: axios.post(url, body, { headers: { Authorization: `Bearer ${token}` } })
  2. I forgot await
    Without awaiting the promise, response was just a pending promise and response.data was undefined.
  3. Axios instance confusion
    I created new Axios() but then called axios.post(...) directly. It’s better to stick with a single axios.create() client.
  4. Body format wasn’t consistent
    The API wanted raw JSON. Axios handles JSON automatically if I set Content-Type: application/json.
  5. 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.

Related blog posts