How to use Redux to refresh JWT token? - javascript

Our React Native Redux app uses JWT tokens for authentication. There are many actions that require such tokens and a lot of them are dispatched simultaneously e.g. when app loads.
E.g.
componentDidMount() {
dispath(loadProfile());
dispatch(loadAssets());
...
}
Both loadProfile and loadAssets require JWT. We save the token in the state and AsyncStorage. My question is how to handle token expiration.
Originally I was going to use middleware for handling token expiration
// jwt-middleware.js
export function refreshJWTToken({ dispatch, getState }) {
return (next) => (action) => {
if (isExpired(getState().auth.token)) {
return dispatch(refreshToken())
.then(() => next(action))
.catch(e => console.log('error refreshing token', e));
}
return next(action);
};
}
The problem that I ran into was that refreshing of the token will happen for both loadProfile and loadAssets actions because at the time when they are dispatch the token will be expired. Ideally I would like to "pause" actions that require authentication until the token is refreshed. Is there a way to do that with middleware?

I found a way to solve this. I am not sure if this is best practice approach and there are probably some improvements that could be made to it.
My original idea stays: JWT refresh is in the middleware. That middleware has to come before thunk if thunk is used.
...
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore);
Then in the middleware code we check to see if token is expired before any async action. If it is expired we also check if we are already are refreshing the token -- to be able to have such check we add promise for fresh token to the state.
import { refreshToken } from '../actions/auth';
export function jwt({ dispatch, getState }) {
return (next) => (action) => {
// only worry about expiring token for async actions
if (typeof action === 'function') {
if (getState().auth && getState().auth.token) {
// decode jwt so that we know if and when it expires
var tokenExpiration = jwtDecode(getState().auth.token).<your field for expiration>;
if (tokenExpiration && (moment(tokenExpiration) - moment(Date.now()) < 5000)) {
// make sure we are not already refreshing the token
if (!getState().auth.freshTokenPromise) {
return refreshToken(dispatch).then(() => next(action));
} else {
return getState().auth.freshTokenPromise.then(() => next(action));
}
}
}
}
return next(action);
};
}
The most important part is refreshToken function. That function needs to dispatch action when token is being refreshed so that the state will contain the promise for the fresh token. That way if we dispatch multiple async actions that use token auth simultaneously the token gets refreshed only once.
export function refreshToken(dispatch) {
var freshTokenPromise = fetchJWTToken()
.then(t => {
dispatch({
type: DONE_REFRESHING_TOKEN
});
dispatch(saveAppToken(t.token));
return t.token ? Promise.resolve(t.token) : Promise.reject({
message: 'could not refresh token'
});
})
.catch(e => {
console.log('error refreshing token', e);
dispatch({
type: DONE_REFRESHING_TOKEN
});
return Promise.reject(e);
});
dispatch({
type: REFRESHING_TOKEN,
// we want to keep track of token promise in the state so that we don't try to refresh
// the token again while refreshing is in process
freshTokenPromise
});
return freshTokenPromise;
}
I realize that this is pretty complicated. I am also a bit worried about dispatching actions in refreshToken which is not an action itself. Please let me know of any other approach you know that handles expiring JWT token with redux.

Instead of "waiting" for an action to finish, you could instead keep a store variable to know if you're still fetching tokens:
Sample reducer
const initialState = {
fetching: false,
};
export function reducer(state = initialState, action) {
switch(action.type) {
case 'LOAD_FETCHING':
return {
...state,
fetching: action.fetching,
}
}
}
Now the action creator:
export function loadThings() {
return (dispatch, getState) => {
const { auth, isLoading } = getState();
if (!isExpired(auth.token)) {
dispatch({ type: 'LOAD_FETCHING', fetching: false })
dispatch(loadProfile());
dispatch(loadAssets());
} else {
dispatch({ type: 'LOAD_FETCHING', fetching: true })
dispatch(refreshToken());
}
};
}
This gets called when the component mounted. If the auth key is stale, it will dispatch an action to set fetching to true and also refresh the token. Notice that we aren't going to load the profile or assets yet.
New component:
componentDidMount() {
dispath(loadThings());
// ...
}
componentWillReceiveProps(newProps) {
const { fetching, token } = newProps; // bound from store
// assuming you have the current token stored somewhere
if (token === storedToken) {
return; // exit early
}
if (!fetching) {
loadThings()
}
}
Notice that now you attempt to load your things on mount but also under certain conditions when receiving props (this will get called when the store changes so we can keep fetching there) When the initial fetch fails, it will trigger the refreshToken. When that is done, it'll set the new token in the store, updating the component and hence calling componentWillReceiveProps. If it's not still fetching (not sure this check is necessary), it will load things.

I made a simple wrapper around redux-api-middleware to postpone actions and refresh access token.
middleware.js
import { isRSAA, apiMiddleware } from 'redux-api-middleware';
import { TOKEN_RECEIVED, refreshAccessToken } from './actions/auth'
import { refreshToken, isAccessTokenExpired } from './reducers'
export function createApiMiddleware() {
const postponedRSAAs = []
return ({ dispatch, getState }) => {
const rsaaMiddleware = apiMiddleware({dispatch, getState})
return (next) => (action) => {
const nextCheckPostponed = (nextAction) => {
// Run postponed actions after token refresh
if (nextAction.type === TOKEN_RECEIVED) {
next(nextAction);
postponedRSAAs.forEach((postponed) => {
rsaaMiddleware(next)(postponed)
})
} else {
next(nextAction)
}
}
if(isRSAA(action)) {
const state = getState(),
token = refreshToken(state)
if(token && isAccessTokenExpired(state)) {
postponedRSAAs.push(action)
if(postponedRSAAs.length === 1) {
return rsaaMiddleware(nextCheckPostponed)(refreshAccessToken(token))
} else {
return
}
}
return rsaaMiddleware(next)(action);
}
return next(action);
}
}
}
export default createApiMiddleware();
I keep tokens in the state, and use a simple helper to inject Acess token into a request headers
export function withAuth(headers={}) {
return (state) => ({
...headers,
'Authorization': `Bearer ${accessToken(state)}`
})
}
So redux-api-middleware actions stays almost unchanged
export const echo = (message) => ({
[RSAA]: {
endpoint: '/api/echo/',
method: 'POST',
body: JSON.stringify({message: message}),
headers: withAuth({ 'Content-Type': 'application/json' }),
types: [
ECHO_REQUEST, ECHO_SUCCESS, ECHO_FAILURE
]
}
})
I wrote the article and shared the project example, that shows JWT refresh token workflow in action

I think that redux is not the right tool for enforcing the atomicity of token refresh.
Instead I can offer you an atomic function that can be called from anywhere and ensures that you will always get a valid token:
/*
The non-atomic refresh function
*/
const refreshToken = async () => {
// Do whatever you need to do here ...
}
/*
Promise locking-queueing structure
*/
var promiesCallbacks = [];
const resolveQueue = value => {
promiesCallbacks.forEach(x => x.resolve(value));
promiesCallbacks = [];
};
const rejectQueue = value => {
promiesCallbacks.forEach(x => x.reject(value));
promiesCallbacks = [];
};
const enqueuePromise = () => {
return new Promise((resolve, reject) => {
promiesCallbacks.push({resolve, reject});
});
};
/*
The atomic function!
*/
var actionInProgress = false;
const refreshTokenAtomically = () => {
if (actionInProgress) {
return enqueuePromise();
}
actionInProgress = true;
return refreshToken()
.then(({ access }) => {
resolveQueue(access);
return access;
})
.catch((error) => {
rejectQueue(error);
throw error;
})
.finally(() => {
actionInProgress = false;
});
};
Posted also here: https://stackoverflow.com/a/68154638/683763

Related

Canceling request of nuxt fetch hook

Since Nuxt's fetch hooks cannot run in parallel, I needed a way to cancel requests done in fetch hook when navigating to some other route so users don't have to wait for the first fetch to complete when landed on the homepage navigated to some other. So I found this approach: How to cancel all Axios requests on route change
So I've created these plugin files for Next:
router.js
export default ({ app, store }) => {
// Every time the route changes (fired on initialization too)
app.router.beforeEach((to, from, next) => {
store.dispatch('cancel/cancel_pending_requests')
next()
})
}
axios.js
export default function ({ $axios, redirect, store }) {
$axios.onRequest((config) => {
const source = $axios.CancelToken.source()
config.cancelToken = source.token
store.commit('cancel/ADD_CANCEL_TOKEN', source)
return config
}, function (error) {
return Promise.reject(error)
})
}
and a small vuex store for the cancel tokens:
export const state = () => ({
cancelTokens: []
})
export const mutations = {
ADD_CANCEL_TOKEN (state, token) {
state.cancelTokens.push(token)
},
CLEAR_CANCEL_TOKENS (state) {
state.cancelTokens = []
}
}
export const actions = {
cancel_pending_requests ({ state, commit }) {
state.cancelTokens.forEach((request, i) => {
if (request.cancel) {
request.cancel('Request canceled')
}
})
commit('CLEAR_CANCEL_TOKENS')
}
}
Now this approach works fine and I can see requests get canceled with 499 on route change, however, it is flooding my devtools console with "Error in fetch()" error. Is there some preferred/better way to do this?
Example of fetch hook here:
async fetch () {
await this.$store.dispatch('runs/getRunsOverview')
}
Example of dispatched action:
export const actions = {
async getRunsOverview ({ commit }) {
const data = await this.$axios.$get('api/frontend/runs')
commit('SET_RUNS', data)
}
}
Edit: I forgot to mention that I'm using fetch here with fetchOnServer set to False to display some loading placeholder to users.
The main problem is the flooded console with error, but I can also see that it also enters the $fetchState.error branch in my template, which displays div with "Something went wrong" text before route switches.
Edit 2:
Looked closer where this error comes from and it's mixin file fetch.client.js in .nuxt/mixins directory. Pasting the fetch function code below:
async function $_fetch() {
this.$nuxt.nbFetching++
this.$fetchState.pending = true
this.$fetchState.error = null
this._hydrated = false
let error = null
const startTime = Date.now()
try {
await this.$options.fetch.call(this)
} catch (err) {
if (process.dev) {
console.error('Error in fetch():', err)
}
error = normalizeError(err)
}
const delayLeft = this._fetchDelay - (Date.now() - startTime)
if (delayLeft > 0) {
await new Promise(resolve => setTimeout(resolve, delayLeft))
}
this.$fetchState.error = error
this.$fetchState.pending = false
this.$fetchState.timestamp = Date.now()
this.$nextTick(() => this.$nuxt.nbFetching--)
}
Have also tried to have everything using async/await as #kissu suggested in comments but with no luck :/

Next/Apollo: How to correctly update apollo cache if the relevant query was run in getInitialProps

I'm using nextjs and apollo (with react hooks). I am trying to update the user object in the apollo cache (I don't want to refetch). What is happening is that the user seems to be getting updated in the cache just fine but the user object that the component uses is not getting updated. Here is the relevant code:
The page:
// pages/index.js
...
const Page = ({ user }) => {
return <MyPage user={user} />;
};
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const user = await apolloClient.query({ query: GetUser }).then(({ data: { user } }) => user);
return { user };
};
export default Page;
And the component:
// components/MyPage.jsx
...
export default ({ user }) => {
const [toggleActive] = useMutation(ToggleActive, {
variables: { id: user.id },
update: proxy => {
const currentData = proxy.readQuery({ query: GetUser });
if (!currentData || !currentData.user) {
return;
}
console.log('user active in update:', currentData.user.isActive);
proxy.writeQuery({
query: GetUser,
data: {
...currentData,
user: {
...currentData.user,
isActive: !currentData.user.isActive
}
}
});
}
});
console.log('user active status:', user.isActive);
return <button onClick={toggleActive}>Toggle active</button>;
};
When I continuously press the button, the console log in the update function shows the user active status as flipping back and forth, so it seems that the apollo cache is getting updated properly. However, the console log in the component always shows the same status value.
I don't see this problem happening with any other apollo cache updates that I'm doing where the data object that the component uses is acquired in the component using the useQuery hook (i.e. not from a query in getInitialProps).

Nuxt auth update auth on custom requests

From nuxt auth website I saw this:
setUserToken(token)
Returns: Promise
Set the auth token and fetch the user using the new token and current strategy.
TIP: This function can properly set the user after registration
this.$auth.setUserToken(token)
.then(() => this.$toast.success('User set!'))
Tried to use it and it said method is undefined, looked up in the source files and none of methods are like this one.
I am not very good with this but, how would I set user and token with nuxt/auth module after registration or anything but login/loginWith?
If there is no option for that why is it there on documentation?
I would also need to know if I need to create custom auth do I need to use both cookies and localstorage or just one of them?
It says that cookies are used for server side and storage for client side.
Can I use just cookies and on nuxtServerInit get cookie for token and set token and user data fetched by api within vuex store? Then use it from there if it is needed?
Nuxt/auth module hurt my brain so long and today I created custom module:
First I have this store structure:
store/
-- index.js
-- mutations.js
-- actions.js
-- state.js
-- getters.js
middleware/
-- redirectIfAuth.js
-- redirectIfNotAuth.js
layouts/
default.vue -> has redirectIfNotAuth.js
guest.vue -> has redirectIfAuth.js
pages/
-- login/
---- index.vue -> uses guest.vue as layout
-- dashboard/
----- index.vue -> uses default.vue as layout without declaration
Inside Index.js I have:
import state from './state'
import * as actions from './actions'
import * as mutations from './mutations'
import * as getters from './getters'
export default {
state,
getters,
mutations,
actions,
modules: {}
}
Inside State.js I have:
export default () => ({
user: null,
token: null,
headers: null
})
Inside Actions.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
// importing server based cookie library
export async function nuxtServerInit ({ commit }, { req, res }) {
// If we have any axios requests we need to add async/await
// And since this works on server mode, we don't need to check is it server
let token = null
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
// If we have token within cookies we get user data from api and we pass Autorization headers with token
if (token !== null && token !== false) {
await axios.get('/api/auth/me', {
headers: {
'Authorization': token
}
}).then((response) => {
// If we get user data we set it to store
commit('setUser', response.data.data)
commit('setToken', token)
commit('setHeaders', token)
}).catch((error) => {
// If we get error, we should logout user by removing data within cookies and store
// Additionally you can create specific code error on backend to check if token is expired or invalid
// and then check for status code and then remove data
commit('setUser', null)
commit('setToken', null)
res.setHeader('Set-Cookie', [`authToken=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`])
// This is only way I found useful for removing cookies from node server
console.warn(error)
})
}
}
Inside Mutations.js I have:
export const setToken = (state, payload) => state.token = payload
export const setUser = (state, payload) => state.user = payload
export const setHeaders = (state, payload) => {
state.headers = {
headers: {
'Authorization': payload
}
}
}
Inside Getters.js I have:
export const getUser = (state) => state.user
export const getToken = (state) => state.token
export const getHeaders = (state) => state.headers
Second I created two middlewares and it seems like nuxt middlewares work on both server and client sides, so I needed to require both libraries for server and client side Then I checked which side it is and then try to get token for further investigations If you include and don't check for server and client but use one of them, your templates wont render but show undefined errors for req on client instead and on server it wont show anything.
Inside redirectIfAuth.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
const Cookie = process.client ? require('js-cookie') : undefined
export default function ({ app, redirect, req }) {
let token = null
if (process.server) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
} else if (process.client) {
token = Cookie.get('authToken')
}
if (token && token !== false) {
app.store.commit('setToken', token)
app.store.commit('setHeaders', token)
if (app.store.state.user) {
if (app.store.state.user.roles.includes('customer')) {
return redirect({
name: 'customer-slug',
params: { slug: app.store.state.user.username }
})
} else if (app.store.state.user.roles.includes('admin')) {
return redirect({
name: 'dashboard'
})
} else {
return redirect({
name: 'index'
})
}
} else {
return redirect({
name: 'index'
})
}
}
}
Inside redirectIfNotAuth.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
const Cookie = process.client ? require('js-cookie') : undefined
export default function ({ app, redirect, route, req }) {
let token = null
if (process.server) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
} else if (process.client) {
token = Cookie.get('authToken')
}
if (token === null || token === false) {
return redirect({
name: 'login',
query: {
redirect: route.fullPath
}
})
}
}
Now we use these middlewares within pages or layouts as:
export default {
middleware: ['redirectIfAuth']
}
Or
export default {
middleware: ['redirectIfNotAuth']
}
Login:
async login () {
if (this.form.email !== '' && this.form.password !== '') {
await this.$axios.post('/api/auth/login', this.form).then((response) => {
this.$store.commit('setUser', response.data.data)
this.$store.commit('setToken', 'Bearer ' + response.data.meta.access_token)
this.$store.commit('setHeaders', 'Bearer ' + response.data.meta.access_token)
Cookie.set('authToken', 'Bearer ' + response.data.meta.access_token, { expires: 365 })
// Cookie.set('authUser', response.data.data, { expires: 365 }) if you need user data within cookies
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect)
}
this.$router.push('/')
})
}
}
Logout:
async logout () {
await this.$axios.post('/api/auth/logout', {}, this.headers)
// Cookie.remove('authUser') if exists
Cookie.remove('authToken')
this.$router.push('/')
}
I hope this helps someone or someone get idea from this to make something else. I had million problems with official nuxt auth and only this helped me sort things out...

How to use vue-resource ($http) and vue-router ($route) in a vuex store?

Before I was getting movie detail from the component's script. The function first check whether the movie ID of the store is same as of the route's param movie ID. If its same then don't get the movie from the server API, or else get the movie from the server API.
It was working fine. But now I am trying to get the movie details from the store's mutation. However I am getting error
Uncaught TypeError: Cannot read property '$route' of undefined
How to use vue-router ($route) to access the params and vue-resource ($http) to get from the server API in vuex store?
store.js:
export default new Vuex.Store({
state: {
movieDetail: {},
},
mutations: {
checkMovieStore(state) {
const routerMovieId = this.$route.params.movieId;
const storeMovieId = state.movieDetail.movie_id;
if (routerMovieId != storeMovieId) {
let url = "http://dev.site.com/api/movies/movie-list/" + routerMovieId + "/";
this.$http.get(url)
.then((response) => {
state.movieDetail = response.data;
})
.catch((response) => {
console.log(response)
});
}
},
},
});
component script:
export default {
computed: {
movie() {
return this.$store.state.movieDetail;
}
},
created: function () {
this.$store.commit('checkMovieStore');
},
}
To use $http or $router in your vuex store, you would need to use the main vue instance. Although I don't recommend using this, I'll add what I recommend after answering the actual question.
In your main.js or wherever you are creating your vue instance like:
new Vue({
el: '#app',
router,
store,
template: '<App><App/>',
components: {
App
}
})
or something similar, you might also have added the vue-router and vue-resource plugins too.
Doing a slight modification to this:
export default new Vue({
el: '#app',
router,
store,
template: '<App><App/>',
components: {
App
}
})
I can now import it in vuex stores like so:
//vuex store:
import YourVueInstance from 'path/to/main'
checkMovieStore(state) {
const routerMovieId = YourVueInstance.$route.params.movieId;
const storeMovieId = state.movieDetail.movie_id;
if (routerMovieId != storeMovieId) {
let url = "http://dev.site.com/api/movies/movie-list/" + routerMovieId + "/";
YourVueInstance.$http.get(url)
.then((response) => {
state.movieDetail = response.data;
})
.catch((response) => {
console.log(response)
});
}
}
and as the answer by Austio goes, this method should be an action as mutations are not designed to handle async.
Now coming to the recommended way of doing it.
Your component can access the route params and provide it to the action.
methods: {
...mapActions({
doSomethingPls: ACTION_NAME
}),
getMyData () {
this.doSomethingPls({id: this.$route.params})
}
}
The action then makes the call through an abstracted API service file (read plugins)
[ACTION_NAME]: ({commit}, payload) {
serviceWhichMakesApiCalls.someMethod(method='GET', payload)
.then(data => {
// Do something with data
})
.catch(err => {
// handle the errors
})
}
Your actions do some async job and provide the result to a mutation .
serviceWhichMakesApiCalls.someMethod(method='GET', payload)
.then(data => {
// Do something with data
commit(SOME_MUTATION, data)
})
.catch(err => {
// handle the errors
})
Mutations should be the only ones to modify your state.
[SOME_MUTATION]: (state, payload) {
state[yourProperty] = payload
}
Example
A file which contains a list of endpoints, you might need it if you have different stages of deployment which have different api endpoints like: test, staging, production, etc.
export const ENDPOINTS = {
TEST: {
URL: 'https://jsonplaceholder.typicode.com/posts/1',
METHOD: 'get'
}
}
And the main file which implements Vue.http as a service:
import Vue from 'vue'
import { ENDPOINTS } from './endpoints/'
import { queryAdder } from './endpoints/helper'
/**
* - ENDPOINTS is an object containing api endpoints for different stages.
* - Use the ENDPOINTS.<NAME>.URL : to get the url for making the requests.
* - Use the ENDPOINTS.<NAME>.METHOD : to get the method for making the requests.
* - A promise is returned BUT all the required processing must happen here,
* the calling component must directly be able to use the 'error' or 'response'.
*/
function transformRequest (ENDPOINT, query, data) {
return (ENDPOINT.METHOD === 'get')
? Vue.http[ENDPOINT.METHOD](queryAdder(ENDPOINT.URL, query))
: Vue.http[ENDPOINT.METHOD](queryAdder(ENDPOINT.URL, query), data)
}
function callEndpoint (ENDPOINT, data = null, query = null) {
return new Promise((resolve, reject) => {
transformRequest(ENDPOINT, query, data)
.then(response => { return response.json() })
.then(data => { resolve(data) })
.catch(error => { reject(error) })
})
}
export const APIService = {
test () { return callEndpoint(ENDPOINTS.TEST) },
login (data) { return callEndpoint(ENDPOINTS.LOGIN, data) }
}
The queryAdder in case it is important, I was using this to add params to the url.
export function queryAdder (url, params) {
if (params && typeof params === 'object' && !Array.isArray(params)) {
let keys = Object.keys(params)
if (keys.length > 0) {
url += `${url}?`
for (let [key, i] in keys) {
if (keys.length - 1 !== i) {
url += `${url}${key}=${params[key]}&`
} else {
url += `${url}${key}=${params[key]}`
}
}
}
}
return url
}
So a few things, the $store and $route are properties of the Vue instance, which is why accessing them inside of Vuex instance is not working. Also, mutations are synchonous what you need are actions
Mutations => A function that given state and some arguments mutates the state
Action => Do async things like http calls and then commit results to a mutation
So create an action that dispatches the http. Keep in mind this is pseudocode.
//action in store
checkMovieStore(store, id) {
return $http(id)
.then(response => store.commit({ type: 'movieUpdate', payload: response })
}
//mutation in store
movieUpdate(state, payload) {
//actually set the state here
Vue.set(state.payload, payload)
}
// created function in component
created: function () {
return this.$store.dispatch('checkMovieStore', this.$route.params.id);
},
Now your created function dispatches the checkMovieStore action with the id, which does the http call, once that is complete it updates the store with the value.
In your vuex store:
import Vue from 'vue'
Vue.http.post('url',{})
Not like in normal vue components:
this.$http.post(...)
I highly recommend importing axios on the vuex module (store and submodules), and using it for your http requests
To access the vue instance in the store use this._vm.
But as Amresh advised do not use things like $router in vuex

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: 'test#test.com',
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)
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:
You fire off submitLogin()
You fire off performRequest(getUsers) before submitLogin() comes back
submitLogin() comes back and stores the credentials from the response headers
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
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.

Categories

Resources