Infinite loop on Redux saga yield call using Axios with JWT tokens - redux

I have been trying to obtain data using Axios through Redux-saga using Redux-toolkit & react. It appears that intercepting a saga call with a token gets redux-saga in an infinite loop? Or is it because of my watchers?
I have recently been learning how to program so my skills in all areas are not yet great, hope you dont mind the way the code is written as I have been following tutorials mostly.
On handleSubmit from a Header.tsx to dispatch
const handleSubmit = (e) => {
e.preventDefault();
dispatch(getCurrentUser());
};
my rootSaga.tsx includes all watcherSagas notices the dispatch for getCurrentUser()
import { takeLatest } from "redux-saga/effects";
import {
handleLogInUser,
handleGetCurrentUser,
handleSetCurrentUser,
} from "./handlers/user";
import {
logInUser,
getCurrentUser,
setCurrentUser,
} from "../slices/user/userSlice";
export function* watcherSaga() {
yield takeLatest(logInUser.type, handleLogInUser);
yield takeLatest(getCurrentUser.type, handleGetCurrentUser);
yield takeLatest(setCurrentUser.type, handleSetCurrentUser);
}
the watcher calls handleGetCurrentUser for the saga located in user.tsx file in handler folder:
import { call, put } from "redux-saga/effects";
import { setCurrentUser } from "../../slices/user/userSlice";
import { requestLogInUser, requestGetCurrentUser } from "../requests/user";
export function* handleLogInUser(action) {
try {
console.log(action + "in handleLogInUser");
yield call(requestLogInUser(action));
} catch (error) {
console.log(error);
}
}
export function* handleGetCurrentUser(action) {
try {
const response = yield call(requestGetCurrentUser);
const userData = response;
yield put(setCurrentUser({ ...userData }));
} catch (error) {
console.log(error);
}
}
Which then uses yield call to requestGetCurrentUser which fires off the request to the following user.tsx in requests folder
import axiosInstance from "../../../axios/Axios";
export function requestGetCurrentUser() {
return axiosInstance.request({ method: "get", url: "/user/currentUser/" });
}
The response is given back and put in const userData, I consoleLog()'d the handler and discovered the following:
it will reach the handler successfully
go to the yield call
obtain the data successfully
return the data back to the handler
then it restarts the entire yield call again?
It also never makes it back to the userSlice in order to put the data.
axiosInstance in my axios.tsx file which includes the interceptor and gets the access_token and adds it to the header.
import axios from "axios";
const baseURL = "http://127.0.0.1:8000/api/";
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
"Content-Type": "application/json",
accept: "application/json",
},
});
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
if (typeof error.response === "undefined") {
alert(
"A server/network error occurred. " +
"Looks like CORS might be the problem. " +
"Sorry about this - we will get it fixed shortly."
);
return Promise.reject(error);
}
if (
error.response.status === 401 &&
originalRequest.url === baseURL + "token/refresh/"
) {
window.location.href = "/login/";
return Promise.reject(error);
}
if (
error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized"
) {
const refreshToken = localStorage.getItem("refresh_token");
if (refreshToken) {
const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
console.log(tokenParts.exp);
if (tokenParts.exp > now) {
return axiosInstance
.post("/token/refresh/", {
refresh: refreshToken,
})
.then((response) => {
localStorage.setItem("access_token", response.data.access);
localStorage.setItem("refresh_token", response.data.refresh);
axiosInstance.defaults.headers["Authorization"] =
"JWT " + response.data.access;
originalRequest.headers["Authorization"] =
"JWT " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
console.log(err);
});
} else {
console.log("Refresh token is expired", tokenParts.exp, now);
window.location.href = "/login/";
}
} else {
console.log("Refresh token not available.");
window.location.href = "/login/";
}
}
// specific error handling done elsewhere
return Promise.reject(error);
}
);
export default axiosInstance;
The userSlice.tsx
import { createSlice } from "#reduxjs/toolkit";
const userSlice = createSlice({
name: "user",
initialState: {},
reducers: {
logInUser(state, action) {},
getCurrentUser() {},
setCurrentUser(state, action) {
const userData = action.payload;
console.log(userData + "we are now back in slice");
return { ...state, ...userData };
},
},
});
export const { logInUser, getCurrentUser, setCurrentUser } = userSlice.actions;
export default userSlice.reducer;
I discovered that if I were to remove the authorization token it only fires off once and gets out of the infinite loop since it throws the unauthorised error.
Any suggestions would be greatly appreciated, thanks!

Apologies for getting back so late, I managed to fix it a while ago by pure chance and I dont exactly understand why.
But I believe what fixed it were the following two things:
Changing the useEffect that dispatched the action and ensuring that the handler returned data that the useEffect was expecting to be updated.
In the handler I deconstructed the userData to { userData } which I believe means that the data returned from the axios request is not the entire request but the actual returned data.
my handler
export function* handleGetCurrentUser() {
try {
console.log("in request get user");
const response = yield call(requestGetCurrentUser);
const { data } = response;
yield put(setCurrentUser({ ...data }));
} catch (error) {
console.log(error);
}
}
I forgot to add my useEffect to the post, which created the action.
my useEffect in the App.tsx would dispatch the call when the App was rendered for the first time. However because the returned data did not update what was expected it kept rerendering.
I cant exactly remember what my useEffect was but currently it is the following:
my useEffect in App.tsx
const dispatch = useDispatch();
useEffect(() => {
dispatch(getCurrentUser());
}, [dispatch]);
const user = useSelector((state) => state.user);

Related

How to handle next api route using dynamic id

I am using NextJS and created three layers that separate the logic.
The purpose is to minimise the error handling to the getServerSideProps section. I want to get all the lists from the database.
In the first layer, in the API route I created a folder called get-all-lists and a file [userId].js. The get request will be 'http://localhost:3000/api/get-all-lists/iudga937gr8'. Bellow there is the api route that get all the lists with the help of Prsima. It is working perfectly
import prisma from '../../../lib/prisma'
export default async function handler(req, res) {
const { userId } = req.query;
if (req.method === 'GET') {
try {
const shoppingLists = await prisma.List.findMany({ where: { userId: userId }});
res.status(200).json({lists: shoppingLists});
}
catch (error) {
console.log(error);
res.status(500).json({ message: 'Something went wrong. Please try again'});
}
}
else {
res.status(500).json({message: 'Invalid method requested!'});
}
}
The next layer, is the abstraction one which sent the final result to getServerSideProps. I created this because I need to fetch alot of requests and it would be too messy...
export const getAllLists = async userId => {
try {
const lists = await axios.get(`/api/get-all-lists/${userId}`);
return lists;
}
catch (error) {
console.log('Abstraction layer error: ', error);
return 'Something went wrong. Please try again later';
}
}
The problem arise here. In the postman I have the right result. In postman I use http://localhost:3000/api/get-all-lists/clbcpc0hi0002sb1wsiea3q5d and the server sent me the array specified.
But this function does not work and send me this error:
Abstraction layer error: TypeError [ERR_INVALID_URL]: Invalid URL
at new NodeError (node:internal/errors:371:5)
at onParseError (node:internal/url:552:9)
at new URL (node:internal/url:628:5)
at dispatchHttpRequest (file:///Users/sasdaniel/Desktop/massage/node_modules/axios/lib/adapters/http.js:176:20)
at new Promise (<anonymous>)
at http (file:///Users/sasdaniel/Desktop/massage/node_modules/axios/lib/adapters/http.js:112:10)
at Axios.dispatchRequest (file:///Users/sasdaniel/Desktop/massage/node_modules/axios/lib/core/dispatchRequest.js:51:10)
at Axios.request (file:///Users/sasdaniel/Desktop/massage/node_modules/axios/lib/core/Axios.js:142:33)
at Axios.<computed> [as get] (file:///Users/sasdaniel/Desktop/massage/node_modules/axios/lib/core/Axios.js:168:17)
at Function.wrap [as get] (file:///Users/sasdaniel/Desktop/massage/node_modules/axios/lib/helpers/bind.js:5:15) {
input: '/api/get-all-lists/clbcpc0hi0002sb1wsiea3q5d',
code: 'ERR_INVALID_URL'
}
I also tried to paste the localhost in the browser and it have no problem.
You could extract the functionality into /lib/getAllList.js:
import prisma from './prisma';
export default async function getAllLists(userId) {
return await prisma.List.findMany({ where: { userId: userId }});
}
Then use it in your API route:
import getAllLists from '../../../lib/getAllLists';
export default async function handler(req, res) {
const { userId } = req.query;
if (req.method === 'GET') {
try {
const shoppingLists = await getAllLists(userId);
res.status(200).json({lists: shoppingLists});
}
catch (error) {
console.log(error);
res.status(500).json({ message: 'Something went wrong. Please try again'});
}
}
else {
res.status(500).json({message: 'Invalid method requested!'});
}
}
Then use it in getServerSideProps:
import getAllLists from 'path/to/lib/getAllLists';
export async function getServerSideProps(context) {
const { userId } = context.params;
const shoppingLists = await getAllLists(userId);
return {
props: {
shoppingLists,
},
};
}

Get supabase `user` server side in next.js

I am attempting to get the current logged in supabase user while server side.
I have attempted to use const user = supabase.auth.user(); but I always get a null response.
I have also attempted const user = supabase.auth.getUserByCookie(req) but it also returns null. I think because I am not sending a cookie to the api when calling it from the hook.
I have tried passing the user.id from the hook to the api but the api is not receiving the parameters.
I also attempted this approach but the token is never fetched. It seems to not exist in req.cookies.
let supabase = createClient(supabaseUrl, supabaseKey);
let token = req.cookies['sb:token'];
if (!token) {
return
}
let authRequestResult = await fetch(`${supabaseUrl}/auth/v1/user`, {
headers: {
'Authorization': `Bearer ${token}`,
'APIKey': supabaseKey
}
});
`
Does anyone know how to get the current logged in user in server side code?
If you need to get the user in server-side, you need to set the Auth Cookie in the server using the given Next.js API.
// pages/api/auth.js
import { supabase } from "../path/to/supabaseClient/definition";
export default function handler(req, res) {
if (req.method === "POST") {
supabase.auth.api.setAuthCookie(req, res);
} else {
res.setHeader("Allow", ["POST"]);
res.status(405).json({
message: `Method ${req.method} not allowed`,
});
}
}
This endpoint needs to be called every time the state of the user is changed, i.e. the events SIGNED_IN and SIGNED_OUT
You can set up a useEffect in _app.js or probably in a User Context file.
// _app.js
import "../styles/globals.css";
import { supabase } from '../path/to/supabaseClient/def'
function MyApp({ Component, pageProps }) {
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
handleAuthChange(event, session)
if (event === 'SIGNED_IN') {
// TODO: Actions to Perform on Sign In
}
if (event === 'SIGNED_OUT') {
// TODO: Actions to Perform on Logout
}
})
checkUser()
return () => {
authListener.unsubscribe()
}
}, [])
return <Component {...pageProps} />;
}
async function handleAuthChange(event, session) {
await fetch('/api/auth', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session }),
})
}
export default MyApp;
You can now handle this user with a state and pass it to the app or whichever way you'd like to.
You can get the user in the server-side in any Next.js Page
// pages/user_route.js
import { supabase } from '../path/to/supabaseClient/def'
export default function UserPage ({ user }) {
return (
<h1>Email: {user.email}</h1>
)
}
export async function getServerSideProps({ req }) {
const { user } = await supabase.auth.api.getUserByCookie(req)
if (!user) {
return { props: {}, redirect: { destination: '/sign-in' } }
}
return { props: { user } }
}
Here's a YouTube Tutorial from Nader Dabit - https://www.youtube.com/watch?v=oXWImFqsQF4
And his GitHub Repository - https://github.com/dabit3/supabase-nextjs-auth
supabase have a library of helpers for managing auth for both client- and server-side auth and fetching in a couple of frameworks including Next.js: https://github.com/supabase/auth-helpers and appears to be the recommended solution for similar problems based on this thread: https://github.com/supabase/supabase/issues/3783
This is how I'm using it in an API handler, but provided you have access to req, you can access the user object this way:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies["sb-access-token"]);
Note that you will need to use the helper library supabaseClient and supabaseServerClient on the client and server side respectively for this to work as intended.
I was following a tutorial today and was having a similar issue and the below is how i managed to fix it.
I've got this package installed github.com/jshttp/cookie which is why i'm calling cookie.parse.
Supabase Instance:
`//../../../utils/supabase`
import { createClient } from "#supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY
);
In my case this was my API page:
import { supabase } from "../../../utils/supabase";
import cookie from "cookie";
import initStripe from "stripe";
const handler = async (req, res) => {
const { user } = await supabase.auth.api.getUserByCookie(req);
if (!user) {
return res.status(401).send("Unathorized");
}
const token = cookie.parse(req.headers.cookie)["sb-access-token"];
supabase.auth.session = () => ({
access_token: token,
});`
const {
data: { stripe_customer },
} = await supabase
.from("profile")
.select("stripe_customer")
.eq("id", user.id)
.single();
For anyone who tries to figure out how to get the user server side with the new #supabase/auth-helpers-nextjs, Michele gave the answer.
Just a note: If you're trying to get the user on nextJs's Middleware, instead of:
... req.cookies["sb-access-token"]
You have to use: req.cookies.get('sb-access-token')
For example:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies.get('sb-access-token'))
UPDATE: 2023. Available now on Supabase Docs here
import { createServerSupabaseClient } from '#supabase/auth-helpers-nextjs'
export default function Profile({ user }) {
return <div>Hello {user.name}</div>
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createServerSupabaseClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}

Handling errors with redux-toolkit

The information about the error in my case sits deeply in the response, and I'm trying to move my project to redux-toolkit. This is how it used to be:
catch(e) {
let warning
switch (e.response.data.error.message) {
...
}
}
The problem is that redux-toolkit doesn't put that data in the rejected action creator and I have no access to the error message, it puts his message instead of the initial one:
While the original response looks like this:
So how can I retrieve that data?
Per the docs, RTK's createAsyncThunk has default handling for errors - it dispatches a serialized version of the Error instance as action.error.
If you need to customize what goes into the rejected action, it's up to you to catch the initial error yourself, and use rejectWithValue() to decide what goes into the action:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response.data)
}
}
)
We use thunkAPI, the second argument in the payloadCreator; containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options: For our example async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) is our payloadCreator with the required arguments;
This is an example using fetch api
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const getExampleThunk = createAsyncThunk(
'auth/getExampleThunk',
async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
try{
const response = await fetch('https://reqrefs.in/api/users/yu');
if (!response.ok) {
return rejectWithValue(response.status)
}
const data = await response.json();
return fulfillWithValue(data)
}catch(error){
throw rejectWithValue(error.message)
}
}
)
Simple example in slice:
const exampleSlice = createSlice({
name: 'example',
initialState: {
httpErr: false,
},
reducers: {
//set your reducers
},
extraReducers: {
[getExampleThunk.pending]: (state, action) => {
//some action here
},
[getExampleThunk.fulfilled]: (state, action) => {
state.httpErr = action.payload;
},
[getExampleThunk.rejected]: (state, action) => {
state.httpErr = action.payload;
}
}
})
Handling Error
Take note:
rejectWithValue - utility (additional option from thunkAPI) that you can return/throw in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action.
For those that use apisauce (wrapper that uses axios with standardized errors + request/response transforms)
Since apisauce always resolves Promises, you can check !response.ok and handle it with rejectWithValue. (Notice the ! since we want to check if the request is not ok)
export const login = createAsyncThunk(
"auth/login",
async (credentials, { rejectWithValue }) => {
const response = await authAPI.signin(credentials);
if (!response.ok) {
return rejectWithValue(response.data.message);
}
return response.data;
}
);

Redux saga is is going to catch() instead of putting the action

I have this action in userActions.js:
const receiveFilialData = (data) => {
return {
type: "RECEIVE_FILIAL_DATA",
payload: data
}
}
export default {
receiveFilialData
}
In my login component, I'm making a dispatch to get some data from a saga:
const Login = props => {
const dispatch = useDispatch()
const filiais = useSelector(state => state.filiais)
useEffect(() => {
dispatch({type: 'GET_FILIAIS_REQUEST'})
}, [dispatch, filiais])
}
In LoginService.js I'm making the HTTP request:
const fetchFiliais = async () => {
const response = await fetch(`${baseUrlApi.baseUrlApi}/filiais`);
const responseJson = await response.json()
return responseJson
}
export default {
fetchFiliais
}
And I create a saga to make this request when there's a GET_FILIAIS_REQUEST dispatch:
// worker Saga: will be fired on GET_FILIAIS_REQUEST actions
function* fetchFiliais(action) {
try {
const data = yield call(LoginService.fetchFiliais())
console.log(data)
yield put({type: "RECEIVE_FILIAL_DATA", data});
} catch (e) {
yield put({type: 'FETCH_FILIAIS_FAILED', message: e.message})
}
}
/*
Starts fetchFiliais on each dispatched `GET_FILIAIS_REQUEST` action.
*/
function* mySaga() {
yield takeEvery("GET_FILIAIS_REQUEST", fetchFiliais);
}
If I put a console.log(response.json) in the LoginService file, fetchFiliais is printing my response, but my console.log(data) after yield call() is not running; the control flow is going to catch(e) and throwing:
type: "FETCH_FILIAIS_FAILED" message: "call: argument of type
{context, fn} has undefined or null fn"
Why?
Fixed, putting LoginService.fetchFiliais instead LoginService.fetchFiliais())
This is not a function but a const

redux refresh token middleware

I have a middleware that can go to the refresh token before the next action runs and then run the other action when the access token expires.
But if I make more than one request at a time and the access token is over, I am trying to get as much refresh token as I am requesting. I am checking the isLoading property in state to prevent this. But after the request, isLoading value is true in the reducer, it seems to be false in the middleware, so it requests again and again.
I am sending refreshTokenPromise in fetching_refresh_token action, but I never get state.refreshTokenPromise, it is always undefined.
I definitely have a problem with the state.
So here is my question, how can I access the changing state value in middleware?
Refresh token middleware: (this version hits the endpoint multiple times)
import { AsyncStorage } from 'react-native';
import { MIN_TOKEN_LIFESPAN } from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
export default function tokenMiddleware({ dispatch, getState }) {
return next => async (action) => {
if (typeof action === 'function') {
const state = getState();
if (state) {
const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
if (expiresIn && isExpired(JSON.parse(expiresIn))) {
if (!state.refreshToken.isLoading) {
return refreshToken(dispatch).then(() => next(action));
}
return state.refreshTokenPromise.then(() => next(action));
}
}
}
return next(action);
};
}
async function refreshToken(dispatch) {
const clientId = await AsyncStorage.getItem('CLIENT_ID');
const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');
const userObject = {
grant_type: 'refresh_token',
client_id: JSON.parse(clientId),
client_secret: JSON.parse(clientSecret),
refresh_token: refreshToken1,
};
const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');
const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => {
await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));
dispatch({
type: FETCHING_REFRESH_TOKEN_SUCCESS,
data: res,
});
return res ? Promise.resolve(res) : Promise.reject({
message: 'could not refresh token',
});
}).catch((err) => {
dispatch({
type: FETCHING_REFRESH_TOKEN_FAILURE,
});
throw err;
});
dispatch({
type: FETCHING_REFRESH_TOKEN,
refreshTokenPromise,
});
return refreshTokenPromise;
}
function isExpired(expiresIn) {
return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;
}
Refresh token reducer:
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
const initialState = {
token: [],
isLoading: false,
error: false,
};
export default function refreshTokenReducer(state = initialState, action) {
switch (action.type) {
case FETCHING_REFRESH_TOKEN:
return {
...state,
token: [],
isLoading: true,
};
case FETCHING_REFRESH_TOKEN_SUCCESS:
return {
...state,
isLoading: false,
token: action.data,
};
case FETCHING_REFRESH_TOKEN_FAILURE:
return {
...state,
isLoading: false,
error: true,
};
default:
return state;
}
}
In the meantime, when I send it to the getState to refreshToken function, I get to the changing state value in the refreshToken. But in this version, the refresh token goes to other actions without being refreshed.
Monkey Patched version: (this version only makes 1 request)
import { AsyncStorage } from 'react-native';
import { MIN_TOKEN_LIFESPAN } from 'react-native-dotenv';
import moment from 'moment';
import Api from '../lib/api';
import {
FETCHING_REFRESH_TOKEN,
FETCHING_REFRESH_TOKEN_SUCCESS,
FETCHING_REFRESH_TOKEN_FAILURE } from '../actions/constants';
export default function tokenMiddleware({ dispatch, getState }) {
return next => async (action) => {
if (typeof action === 'function') {
const state = getState();
if (state) {
const expiresIn = await AsyncStorage.getItem('EXPIRES_IN');
if (expiresIn && isExpired(JSON.parse(expiresIn))) {
if (!state.refreshTokenPromise) {
return refreshToken(dispatch, getState).then(() => next(action));
}
return state.refreshTokenPromise.then(() => next(action));
}
}
}
return next(action);
};
}
async function refreshToken(dispatch, getState) {
const clientId = await AsyncStorage.getItem('CLIENT_ID');
const clientSecret = await AsyncStorage.getItem('CLIENT_SECRET');
const refreshToken1 = await AsyncStorage.getItem('REFRESH_TOKEN');
const userObject = {
grant_type: 'refresh_token',
client_id: JSON.parse(clientId),
client_secret: JSON.parse(clientSecret),
refresh_token: refreshToken1,
};
if (!getState().refreshToken.isLoading) {
const userParams = Object.keys(userObject).map(key => encodeURIComponent(key) + '=' + encodeURIComponent(userObject[key])).join('&');
const refreshTokenPromise = Api.post('/token', userParams).then(async (res) => {
await AsyncStorage.setItem('ACCESS_TOKEN', res.access_token);
await AsyncStorage.setItem('REFRESH_TOKEN', res.refresh_token);
await AsyncStorage.setItem('EXPIRES_IN', JSON.stringify(res['.expires']));
dispatch({
type: FETCHING_REFRESH_TOKEN_SUCCESS,
data: res,
});
return res ? Promise.resolve(res) : Promise.reject({
message: 'could not refresh token',
});
}).catch((err) => {
dispatch({
type: FETCHING_REFRESH_TOKEN_FAILURE,
});
throw err;
});
dispatch({
type: FETCHING_REFRESH_TOKEN,
refreshTokenPromise,
});
return refreshTokenPromise;
}
}
function isExpired(expiresIn) {
return moment(expiresIn).diff(moment(), 'seconds') < MIN_TOKEN_LIFESPAN;
}
Thank you.
I solved this problem using axios middlewares. I think is pretty nice.
import { AsyncStorage } from 'react-native';
import Config from 'react-native-config';
import axios from 'axios';
import { store } from '../store';
import { refreshToken } from '../actions/refreshToken'; // eslint-disable-line
const instance = axios.create({
baseURL: Config.API_URL,
});
let authTokenRequest;
function resetAuthTokenRequest() {
authTokenRequest = null;
}
async function getAuthToken() {
const clientRefreshToken = await AsyncStorage.getItem('clientRefreshToken');
if (!authTokenRequest) {
authTokenRequest = store.dispatch(refreshToken(clientRefreshToken));
authTokenRequest.then(
() => {
const {
token: { payload },
} = store.getState();
// save payload to async storage
},
() => {
resetAuthTokenRequest();
},
);
}
return authTokenRequest;
}
instance.interceptors.response.use(
response => response,
async (error) => {
const originalRequest = error.config;
if (
error.response.status === 401
&& !originalRequest._retry // eslint-disable-line no-underscore-dangle
) {
return getAuthToken()
.then(() => {
const {
token: {
payload: { 'access-token': accessToken, client, uid },
},
} = store.getState();
originalRequest.headers['access-token'] = accessToken;
originalRequest.headers.client = client;
originalRequest.headers.uid = uid;
originalRequest._retry = true; // eslint-disable-line no-underscore-dangle
return axios(originalRequest);
})
.catch(err => Promise.reject(err));
}
return Promise.reject(error);
},
);
export default instance;
If you have a problem, do not hesitate to ask.
you could benefit from redux-sagas
https://github.com/redux-saga/redux-saga
redux-sagas is just background runner which monitors your actions and can react when some specific action is met. You can listen for all actions and react to all or you can react to only latest as mentioned in comments
https://redux-saga.js.org/docs/api/#takelatestpattern-saga-args
while redux-thunk is just another way to create actions on the go and wait for some I/O to happen and then create some more actions when I/O is done. It's more like synced code pattern and redux-sagas is more like multi-threaded. On main thread you have your app running and on background thread you have sagas monitors and reactions

Resources