Site icon FSIBLOG

How to Resolve 401 Error in React JS Using Redux?

How to Resolve 401 Error in React JS Using Redux?

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:

// 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.

Exit mobile version