MOBX is concatenating an observable instead of updating it

I am building a login page with Mobx, MUI V5, react-router V6 and react-hook-form.

My first API call is to authenticate the application, apiAuth() will return a token that needs to be passed to all subsequent API calls. On the next call, userAuth(), I try to validate the user credential. As you can see, the method takes 3 arguments (a token, card number and, password)

  • When the user credentials are valid, I can login successfully.
  • When the user credentials are not valid on the first try, it works as expected. I receive 400 (Bad Request) error from the API and display the error message on the interface.

That said when I entered the user credentials once more, I get a 401 (Unauthorized) error.

Upon further inspection of the request headers, when I compared the authorization header in both userAuth() calls, I see that the token's value on the second call was concatenated with the previous token

(Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Impvbm55YiIsIm5iZiI6MTY0MjM3NzQwNiwiZXhwIjoxNjQyOTgyMjA2LCJpYXQiOjE2NDIzNzc0MDZ9.mrbMqG62b69PxTlsBNfaZx98nJkcKQp6elSPBMZpOoM, Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Impvbm55YiIsIm5iZiI6MTY0MjM3NzQ0MywiZXhwIjoxNjQyOTgyMjQzLCJpYXQiOjE2NDIzNzc0NDN9.nfk4c5dr3iL0VgbMw8zbnd1bxrCh5FERW0jDTDL20L0)

Any ideas as to why for this behavior?

My AuthStore looks as follow:

class AuthStore {
    isAuth = false
    isAuthFail = false
    AuthFailObj = {}
    bearerToken = '' 
    cardNum = ''
    password=''

    constructor() {
        makeObservable(this, {
            isAuth: observable,
            AuthFailObj: observable,
            isAuthFail:observable,
            bearerToken: observable,
            cardNum: observable,
            password: observable,
            auth: action,
            setIsAuth: action,
            setToken: action,
            setCardNum: action,
            setPassword: action,
            setIsAuthFail: action,
            setAuthFailObj: action
        })
    }

    setIsAuth = isAuth => {
        this.isAuth = isAuth
    }
    
    setToken = bearerToken => {
        this.bearerToken = bearerToken
    }
    
    setCardNum = cardNum => {
        this.cardNum = cardNum
    }

    setPassword = password => {
        this.password = password
    }

    setIsAuthFail = b => {
        this.isAuthFail = b
    }

    setAuthFailObj = ojb => {
        this.AuthFailObj = ojb
    }

    auth = async () => {

        const apiRes = await apiAuth()
        
        if (apiRes.status === 200){
            const apiData = await apiRes.text()
            this.setToken(JSON.parse(apiData)[0].token)
        }
        
        const userAuthRes = await userAuth(this.bearerToken, this.password, this.cardNum)

        if (!userAuthRes.ok){
            this.setIsAuthFail(true)
            const errRes = await userAuthRes.text()
            userAuthRes.status === 400 && this.setAuthFailObj(JSON.parse(errRes))
            userAuthRes.status === 401 && this.setAuthFailObj('401 (Unauthorized)')
        }
        
        if (userAuthRes.ok){
            const userAuthData = await userAuthRes.text()
            userStore.updateUserProfile(JSON.parse(userAuthData))
            this.setIsAuth(true)
        }
    }

}

export default new AuthStore()

In the login form, the submit method looks like this:

const submit = async (data) => {
    AuthStore.setCardNum(data.Card_Number)
    AuthStore.setPassword(data.Password)
    setToggle(true)
    await AuthStore.auth()
    if (AuthStore.isAuth) {
      navigate('/dashboard')
    } else {
      // clear form
    }
  }

Finally, the PrivateRoute logic reads is simple:

const PrivateRoute = () => {
      return AuthStore.isAuth ? <Outlet /> : <Navigate to='/' />
    }

The function userAuth()

const myHeaders = new window.Headers()
const { REACT_APP_API_ACC_MNG_AUTH_URL } = process.env

const userAuth = async (bearerToken, password, cardNum) => {
  myHeaders.append('Authorization', `Bearer ${bearerToken}`)
  myHeaders.append('Content-Type', 'application/json')

  const raw = JSON.stringify({
    cardNumber: cardNum,
    pinNumber: password
  })

  const requestOptions = {
    method: 'POST',
    headers: myHeaders,
    body: raw,
    redirect: 'follow'
  }

  const response = await window.fetch(REACT_APP_API_ACC_MNG_AUTH_URL, requestOptions)
  return response
}


Solution 1:

The issue is that you're using the Headers API and appending to the headers instead of setting them, which exist outside the function scope and are updated. From MDN:

The append() method of the Headers interface appends a new value onto an existing header inside a Headers object, or adds the header if it does not already exist.

So every time you make a request, if you append the header, it will be added on to the existing value. You could move your headers declaration inside of the function, and create a new object each time you make a request:

const { REACT_APP_API_ACC_MNG_AUTH_URL } = process.env

const userAuth = async (bearerToken, password, cardNum) => {
  const myHeaders = new window.Headers()
  myHeaders.append('Authorization', `Bearer ${bearerToken}`)
  myHeaders.append('Content-Type', 'application/json')

  const raw = JSON.stringify({
    cardNumber: cardNum,
    pinNumber: password
  })

  const requestOptions = {
    method: 'POST',
    headers: myHeaders,
    body: raw,
    redirect: 'follow'
  }

  const response = await window.fetch(REACT_APP_API_ACC_MNG_AUTH_URL, requestOptions)
  return response
}

Or you could just pass them in as an object, which is allowed by the Fetch API:

const userAuth = async (bearerToken, password, cardNum) => {

  const raw = JSON.stringify({
    cardNumber: cardNum,
    pinNumber: password
  });

  const requestOptions = {
    method: 'POST',
    headers: myHeaders,
    body: raw,
    redirect: 'follow',
    headers: {
      'Authorization': `Bearer ${bearerToken}`,
      'Content-Type': 'application/json',
    }
  };

  const response = await window.fetch(REACT_APP_API_ACC_MNG_AUTH_URL, requestOptions);
  return response;
}