TWONEKSONE
TWONEKSONE

Reputation: 4258

Redux async requests with fetch api

I'm stuck in a wierd behaviour that I can't really debug.

The store dispatch the action that perform the login request passing username and password. Then when the response is ready I store the credentials in the redux store. When I need to perform an authorized request I set those parameters in the header request. When I receive the response I update the credentials in the store with the new ones that I get from the response. When I try to perform the third request it will respond unauthorized. I figured out that this is because all the parameters passed to my action generator setCredentials are null. I can't understand why also because if I add a debugger before the return statement of my setCredentials function and I wait some seconds before restart the execution I found out that the parameters aren't null anymore. I was thinking about the fact that the request is async but being inside a then statement the response should be ready right? I've also notice that fetch sent two request for each one. Here the code for more clarity.

import { combineReducers } from 'redux'
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';

const initialState = {
  currentUser: {
    credentials: {},
    user: {}
  },
  test: {},
  users: []
}

export const SUBMIT_LOGIN = 'SUBMIT_LOGIN'
export const SET_USER = 'SET_USER'
export const TEST = 'TEST'
export const SET_USERS = 'SET_USERS'
export const SET_CREDENTIALS = 'SET_CREDENTIALS'

//actions
const submitLogin = () => (dispatch) => {
  return postLoginRequest()
    .then(response => {
      dispatch(setCredentials(
        response.headers.get('access-token'),
        response.headers.get('client'),
        response.headers.get('expiry'),
        response.headers.get('token-type'),
        response.headers.get('uid')
      ));
      return response
    })
    .then(response => {
      return response.json();
    })
    .then(
      (user) => dispatch(setUser(user.data)),
    );
}

const performRequest = (api) => (dispatch) => {
  return api()
    .then(response => {
      dispatch(setCredentials(
        response.headers.get('access-token'),
        response.headers.get('client'),
        response.headers.get('expiry'),
        response.headers.get('token-type'),
        response.headers.get('uid')
      ));
      return response
    })
    .then(response => {return response.json()})
    .then(
      (users) => {
        dispatch(setUsers(users.data))
      },
    );
}

const setUsers = (users) => {
  return {
    type: SET_USERS,
    users
  }
}

const setUser = (user) => {
  return {
    type: SET_USER,
    user
  }
}

const setCredentials = (
  access_token,
  client,
  expiry,
  token_type,
  uid
) => {
  debugger
  return {
    type: SET_CREDENTIALS,
    credentials: {
      'access-token': access_token,
      client,
      expiry,
      'token-type': token_type,
      uid
    }
  }
}

//////////////
const currentUserInitialState = {
  credentials: {},
  user: {}
}

const currentUser = (state = currentUserInitialState, action) => {
  switch (action.type) {
    case SET_USER:
      return Object.assign({}, state, {user: action.user})
    case SET_CREDENTIALS:
      return Object.assign({}, state, {credentials: action.credentials})
    default:
      return state
  }
}

const rootReducer = combineReducers({
  currentUser,
  test
})

const getAuthorizedHeader = (store) => {
  const credentials = store.getState().currentUser.credentials
  const headers = new Headers(credentials)
  return headers
}

//store creation

const createStoreWithMiddleware = applyMiddleware(
  thunk
)(createStore);

const store = createStoreWithMiddleware(rootReducer);

const postLoginRequest = () => {
  return fetch('http://localhost:3000/auth/sign_in', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email: '[email protected]',
      password: 'password',
    })
  })
}

const getUsers = () => {
  const autorizedHeader = getAuthorizedHeader(store)
  return fetch('http://localhost:3000/users',
    {
      method: 'GET',
      headers : autorizedHeader
    }
  )
}

const getWorks = () => {
  const autorizedHeader = getAuthorizedHeader(store)
  return fetch('http://localhost:3000/work_offers',
    {
      method: 'GET',
      headers : autorizedHeader
    }
  )
}
// this request works fine
store.dispatch(submitLogin())

// this request works fine
setTimeout(() => {
  store.dispatch(performRequest(getUsers))
}, 3000)

// this fails
setTimeout(() => {
  store.dispatch(performRequest(getWorks))
}, 5000)

Upvotes: 2

Views: 2562

Answers (1)

Dan Abramov
Dan Abramov

Reputation: 268293

I should have clarified that when I asked

Have you verified that all your endpoints return those headers and not just the login one? Maybe when you performRequest(getUsers), it comes back with empty headers.

I didn’t just mean the server logic. I meant opening the Network tab in DevTools and actually verifying whether your responses contain the headers you expect. It turns out getUsers() headers do not always contain the credentials:

Now that we confirmed this happens, let’s see why.

You dispatch submitLogin() and performRequest(getUsers) roughly at the same time. In the cases when the error is reproduced, the problem is in the following sequence of steps:

  1. You fire off submitLogin()
  2. You fire off performRequest(getUsers) before submitLogin() comes back
  3. submitLogin() comes back and stores the credentials from the response headers
  4. performRequest(getUsers) comes back but since it started before credentials were available, the server responds with empty headers, and those empty credentials are stored instead of the existing ones
  5. performRequest(getWorks) is now requested without the credentials

There are several fixes for this problem.

Don’t Let Old Unauthorized Requests Overwrite the Credentials

I don’t think it really makes sense to overwrite existing good credentials with the empty ones, does it? You can either check that they are non-empty in performRequest before dispatching:

const performRequest = (api) => (dispatch, getState) => {
  return api()
    .then(response => {
      if (response.headers.get('access-token')) {
        dispatch(setCredentials(
          response.headers.get('access-token'),
          response.headers.get('client'),
          response.headers.get('expiry'),
          response.headers.get('token-type'),
          response.headers.get('uid')
        ));
      }
      return response
    })
    .then(response => {return response.json()})
    .then(
      (users) => {
        dispatch(setUsers(users.data))
      },
    );
}

Alternatively, you can do ignore invalid credentials in the reducer itself:

case SET_CREDENTIALS:
  if (action.credentials['access-token']) {
    return Object.assign({}, state, {credentials: action.credentials})
  } else {
    return state
  }

Both ways are fine and depend on the conventions that make more sense to you.

Wait Before Performing Requests

In any case, do you really want to fire getUsers() before you have the credentials? If not, fire off the requests only until the credentials are available. Something like this:

store.dispatch(submitLogin()).then(() => {
  store.dispatch(performRequest(getUsers))
  store.dispatch(performRequest(getWorks))
})

If it’s not always feasible or you would like more sophisticated logic like retrying failed requests, I suggest you to look at Redux Saga which lets you use powerful concurrency primitives to schedule this kind of work.

Upvotes: 2

Related Questions