How to handle common fetch actions inside saga - reactjs

I'm developping an API consuming web front site.
The problem
All my API saga were like this :
export function* login(action) {
const requestURL = "./api/auth/login"; // Endpoint URL
// Select the token if needed : const token = yield select(makeSelectToken());
const options = {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + btoa(JSON.stringify({ login: action.email, password: action.password })),
}
};
try {
// The request helper from react-boilerplate
const user = yield call(request, requestURL, options);
yield put(loginActions.loginSuccess(user.token);
yield put(push('/'));
} catch (err) {
yield put(loginActions.loginFailure(err.detailedMessage));
yield put(executeErrorHandler(err.code, err.detailedMessage, err.key)); // Error handling
}
}
And I had the same pattern with all my sagas :
Select the token if I need to call a private function in the start of the saga
const token = yield select(makeSelectToken());
Handle errors on the catch part
export const executeErrorHandler = (code, detailedMessage, key) => ({
type: HTTP_ERROR_HANDLER, status: code, detailedMessage, key
});
export function* errorHandler(action) {
switch (action.status) {
case 400:
yield put(addError(action.key, action.detailedMessage));
break;
case 401:
put(push('/login'));
break;
//other errors...
}
}
export default function* httpError() {
yield takeLatest(HTTP_ERROR_HANDLER, errorHandler);
}
The solution I came up with
Remove the token parts and error handling part and puth them inside the call helper :
export function* login(action) {
const url = `${apiUrl.public}/signin`;
const body = JSON.stringify({
email: action.email,
password: action.password,
});
try {
const user = yield call(postRequest, { url, body });
yield put(loginSuccess(user.token, action.email));
yield put(push('/'));
} catch (err) {
yield put(loginFailure());
}
}
// post request just call the default request with a "post" method
export function postRequest({ url, headers, body, auth = null }) {
return request(url, 'post', headers, body, auth);
}
export default function request(url, method, headers, body, auth = null) {
const options = { method, headers, body };
return fetch(url, addHeader(options, auth)) // add header will add the token if auth == true
.then(checkStatus)
.then(parseJSON)
.catch(handleError); // the error handler
}
function handleError(error) {
if (error.code === 401) {
put(push('/login')); // <-- Here this doesn't work
}
if (error.code == 400) {
displayToast(error);
}
}
function addHeader(options = {}, auth) {
const newOptions = { ...options };
if (!options.headers) {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
...options.headers,
};
}
if (auth) {
const token = yield select(makeSelectToken()); // <-- here it doesn't work
newOptions.headers.Authorization = `Bearer ${auth}`;
}
return newOptions;
}
I know the solution is between generator functions, side effects, yield call / select but I tried so many things it didn't work. For example, if I wrap everything inside generator functions, the token load is executed after the code continues and call the API.
Your help would be appreciated.

You need to run any and all effects (e.g. yield select) from a generator function, so you'll need generators all the way down to the point in your call stack where you yield an effect. Given that I would try to push those calls as high as possible. I assume you may have getRequest, putRequest etc. in addition to postRequest so if you want to avoid duplicating the yield select you'll want to do it in request. I can't fully test your snippet but I believe this should work:
export function* postRequest({ url, headers, body, auth = null }) {
return yield call(request, url, 'post', headers, body, auth); // could yield directly but using `call` makes testing eaiser
}
export default function* request(url, method, headers, body, auth = null) {
const options = { method, headers, body };
const token = auth ? yield select(makeSelectToken()) : null;
try {
const response = yield call(fetch, url, addHeader(options, token));
const checkedResponse = checkStatus(response);
return parseJSON(checkedResponse);
} catch (e) {
const errorEffect = getErrorEffect(e); // replaces handleError
if (errorEffect) {
yield errorEffect;
}
}
}
function addHeader(options = {}, token) {
const newOptions = { ...options };
if (!options.headers) {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
...options.headers,
};
}
if (token) {
newOptions.headers.Authorization = `Bearer ${token}`;
}
return newOptions;
}
function getErrorEffect(error) {
if (error.code === 401) {
return put(push('/login')); // returns the effect for the `request` generator to yeild
}
if (error.code == 400) {
return displayToast(error); // assuming `displayToast` is an effect that can be yielded directly
}
}

Related

Not getting the localstorage token in my react component

I have token.ts which looks like this -
export const contentTypeHeaderWithToken = {
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`
}
};
In my reduxtoolkit, I am using this contentTypeHeaderWithToken like this -
export const fetchAllRoles = createAsyncThunk<IGetRolesResponse>(
'user/getroles',
async (user, {rejectWithValue}) => {
try {
const response = await Services.get('/roles', contentTypeHeaderWithToken);
return response.data.slice().reverse() as IGetRolesResponse;
} catch(err) {
const error = err as any;
return rejectWithValue(error?.response?.data)
}
},
);
In try statement above, contentTypeHeaderWithToken is coming out to be null for the very first time.
Before pulling the token value, I have verified, the token is present in the browser.
Any idea what I am missing ?

Authorization headers taking the previous token value redux toolkit

I'm using redux toolkit and i want to send the token to verify if the user is logged in or not so he can like a post.
My code is as follow :
const userInfo = localStorage.getItem('userInfo') ?
JSON.parse(localStorage.getItem('userInfo')) : null
const config = {
headers: {
Accept: "application/json",
'Content-type': 'application/json',
}
}
const auth = {
headers: {
Authorization: `Bearer ${userInfo?.token}`
}
}
export const likePost = createAsyncThunk("posts/likePost",
async (id,{ rejectWithValue }) => {
try {
const { data } = await axios.patch(`http://localhost:5000/posts/like/${id}/`,
config,
auth,
id,
)
console.log(userInfo?.token)
return data
} catch (error) {
return rejectWithValue(error.response.data)
}
}
)
when i check the headers i find that it takes the token of the previous logged in person

How to select the state from within Redux-Saga

I have a react app where I'm creating an unit and it requires authorization.
function* createUnitWorker(action) {
const { payload: {unitDetails, history} } = action;
try {
const unit = yield axios({
method: 'post',
url: `https://myBackend/units/`,
headers: {'Authorization': 'Bearer'+token},
data: {
...unitDetails
}
});
yield put(call(createUnitSuccess, unit));
yield history.push(`/unit/${unit.code}`)
} catch (error) {
yield put(createUnitFailure(error));
yield put(history.push('/error'))
}
}
export function* createUnitWatcher() {
yield takeLatest(unitActionTypes.CREATE_UNIT_START, createUnitWorker);
}
Should I send the token from the component as part of the payload or should I select the token from the user state I have stored in the saga?. Because it seems to me that it is complicated to select the token mapStateToProps and then send it with the action when I could just select the token from within the saga
I would recommend to use axios interceptors so you don't have to manually add the token to each request you send.
const axiosInstance = axios.create({
baseURL: 'https://baseUrl.com',
headers: { "Content-Type": "application/json" }
});
axiosInstance.interceptors.request.use(function (config) {
config.headers.Authorization = 'Bearer'+token;
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});
This means you can just use the axiosInstance in each saga, without worrying about the token. Something like this:
axiosInstance.post('/units', payload);

Exception not thrown inside save saga

I am working on an SPA with redux-saga state management. My load and save methods themselves are working, yet there is a lot of weird stuff... Below is the saga code:
export function* getEventDetails({ id }) {
const requestURL = `${url}/admin/event/${id}`
try {
const event = yield call(request, requestURL)
yield put(eventLoaded(event, id))
} catch (err) {
yield put(eventLoadingError(err))
}
}
export function* saveEventDetails({ event }) {
const id = event['id']
const requestURL = `${url}/admin/event/${
!isNaN(id) && id !== undefined && id !== null ? id : 'new'
}`
try {
const createdEvent = yield call(request, requestURL, {
method: !isNaN(id) && id !== undefined && id !== null ? 'PUT' : 'POST',
body: JSON.stringify(event)
})
yield put(eventSaved(createdEvent, createdEvent['id']))
yield put(loadEvent(createdEvent['id']))
yield put(loadPreviousEvents())
yield put(loadUpcomingEvents())
} catch (err) {
console.log('caught error inside saga')
yield put(eventSavingError(err))
}
}
export default function* eventsData() {
yield takeLatest(LOAD_EVENT, getEventDetails)
yield takeLatest(SAVE_EVENT, saveEventDetails)
}
One thing is definitely strange - if I turn off the API server then try saving, I never see caught error inside saga in the console. I am therefore unable to dispatch the eventSavingError action, etc.
Where is my error action? In the console I see:
reducer.js:48 action: {type: "project/Container/SAVE_EVENT", event: {…}}
request.js:55 PUT http://localhost:5000/event/10 net::ERR_CONNECTION_REFUSED
The request function:
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response
}
const error = new Error(response.statusText)
error.response = response
throw error
}
export default function request(url, options) {
const headers = {
Accept: 'application/json',
'Content-Type': 'application/json',
'Access-Control-Request-Headers': 'Content-Type, Authorization'
}
const token = localStorage.getItem('token')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const newOptions = {
...options,
mode: 'cors',
headers
}
return fetch(url, newOptions)
.then(checkStatus)
.then(parseJSON)
}
Using #oozywaters suggestion, I tweaked the code as:
return fetch(url, newOptions)
.then(checkStatus)
.then(parseJSON)
.catch(err => {
throw err
})
It does fix the problem with the missing exception.

React native saga yield call is not working

I am trying to write an api by using redux-saga. I have my servicesSaga.js like this
import { FETCH_USER } from '../actions/actionTypes'
import { delay } from 'redux-saga'
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import { _getUserInformation } from './api'
const getUserInformation = function*(action) {
console.log("FONKSİYONA geldi")
console.log(action)
try {
console.log("try catche geldi")
const result = yield call(_getUserInformation, action)
console.log("result döndü")
if (result === true) {
yield put({ type: FETCH_USER })
}
} catch (error) {
}
}
export function* watchGetUserInformation() {
yield takeLatest(FETCH_USER, getUserInformation)
console.log("WatchUsere geldi")
}
I am trying to yeild call my _getUserInformation method from ./api but yield call method is not working.This is my api.js.
const url = 'http://myreduxproject.herokuapp.com/kayitGetir'
function* _getUserInformation(user) {
console.log("Apiye geldi" + user)
const response = yield fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: user.email,
})
})
console.log(response.data[0])
return yield (response.status === 201)
}
export const api ={
_getUserInformation
}
Thank you for your helps from now.
generator function must be define as function* yourFunction() {} try this changes.
servicesSaga.js
function* getUserInformation(action) {
try {
const result = yield _getUserInformation(action) //pass user here
if (result) {
yield put({ type: FETCH_USER })
}
} catch (error) {
}
}
export function* watchGetUserInformation() {
yield takeLatest(FETCH_USER, getUserInformation)
}
api.js
const url = 'http://myreduxproject.herokuapp.com/kayitGetir'
function* _getUserInformation(user) {
const response = yield fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: user.email,
})
})
console.log('response',response);
return response;
}
export {
_getUserInformation
}

Resources