Dispatch async redux action from non-react component with thunk middleware - reactjs

I am building an react / redux webapp where I am using a service to make all my API calls. Whenever the API returns 401 - Unauthorized I want to dispatch a logout action to my redux store.
The problem is now that my api-service is no react component, so I cannot get a reference to dispatch or actions.
What I did first was exporting the store and calling dispatch manually, but as I read here How to dispatch a Redux action with a timeout? that seems to be a bad practice because it requires the store to be a singleton, which makes testing hard and rendering on the server impossible because we need different stores for each user.
I am already using react-thunk (https://github.com/gaearon/redux-thunk) but I dont see how I can injectdispatch` into non-react components.
What do I need to do? Or is it generally a bad practice to dispatch actions outside from react components?
This is what my api.services.ts looks like right now:
... other imports
// !!!!!-> I want to get rid of this import
import {store} from '../';
export const fetchWithAuth = (url: string, method: TMethod = 'GET', data: any = null): Promise<TResponseData> => {
let promise = new Promise((resolve, reject) => {
const headers = {
"Content-Type": "application/json",
"Authorization": getFromStorage('auth_token')
};
const options = {
body: data ? JSON.stringify(data) : null,
method,
headers
};
fetch(url, options).then((response) => {
const statusAsString = response.status.toString();
if (statusAsString.substr(0, 1) !== '2') {
if (statusAsString === '401') {
// !!!!!-> here I need to dispatch the logout action
store.dispatch(UserActions.logout());
}
reject();
} else {
saveToStorage('auth_token', response.headers.get('X-TOKEN'));
resolve({
data: response.body,
headers: response.headers
});
}
})
});
return promise;
};
Thanks!

If you are using redux-thunk, you can return a function from an action creator, which has dispatch has argument:
const doSomeStuff = dispatch => {
fetch(…)
.then(res => res.json())
.then(json => dispatch({
type: 'dostuffsuccess',
payload: { json }
}))
.catch(err => dispatch({
type: 'dostufferr',
payload: { err }
}))
}
Another option is to use middleware for remote stuff. This works the way, that middle can test the type of an action and then transform it into on or multiple others. have a look here, it is similar, even if is basically about animations, the answer ends with some explanation about how to use middleware for remote requests.

maybe you can try to use middleware to catch the error and dispatch the logout action,
but in that case, the problem is you have to dispatch error in action creator which need to check the log status
api: throw the error
if (statusAsString === '401') {
// !!!!!-> here I need to dispatch the logout action
throw new Error('401')
}
action creator: catch error from api, and dispatch error action
fetchSometing(ur)
.then(...)
.catch(err => dispatch({
type: fetchSometingError,
err: err
})
middleware: catch the error with 401 message, and dispatch logout action
const authMiddleware = (store) => (next) => (action) => {
if (action.error.message === '401') {
store.dispatch(UserActions.logout())
}
}

You should have your api call be completely independent from redux. It should return a promise (like it currently does), resolve in the happy case and reject with a parameter that tells the status. Something like
if (statusAsString === '401') {
reject({ logout: true })
}
reject({ logout: false });
Then in your action creator code you would do:
function fetchWithAuthAction(url, method, data) {
return function (dispatch) {
return fetchWithAuth(url, method, data).then(
({ data, headers }) => dispatch(fetchedData(data, headers)),
({ logout }) => {
if(logout) {
dispatch(UserActions.logout());
} else {
dispatch(fetchedDataFailed());
}
);
};
}
Edit:
If you don't want to write the error handling code everywhere, you could create a helper:
function logoutOnError(promise, dispatch) {
return promise.catch(({ logout }) => {
if(logout) {
dispatch(UserActions.logout());
}
})
}
Then you could just use it in your action creators:
function fetchUsers() {
return function (dispatch) {
return logoutOnError(fetchWithAuth("/users", "GET"), dispatch).then(...)
}
}

You can also use axios (interceptors) or apisauce (monitors) and intercept all calls before they goes to their handlers and at that point use the
// this conditional depends on how the interceptor works on each api.
// In apisauce you use response.status
if (response.status === '401') {
store.dispatch(UserActions.logout())
}

Related

React and Redux toolkit - reject after promise

I'm working on a React Native app. I have a signup screen which has a button, onclick:
const handleClick = (country: string, number: string): void => {
dispatch(registerUser({ country, number }))
.then(function (response) {
console.log("here", response);
navigation.navigate(AuthRoutes.Confirm);
})
.catch(function (e) {
console.log('rejected', e);
});
};
The registerUser function:
export const registerUser = createAsyncThunk(
'user/register',
async ({ country, number }: loginDataType, { rejectWithValue }) => {
try {
const response = await bdzApi.post('/register', { country, number });
return response.data;
} catch (err) {
console.log(err);
return rejectWithValue(err.message);
}
},
);
I have one of my extraReducers that is indeed called, proving that it's effectively rejected.
.addCase(registerUser.rejected, (state, {meta,payload,error }) => {
state.loginState = 'denied';
console.log(`nope : ${JSON.stringify(payload)}`);
})
But the signup component gets processed normally, logging "here" and navigating to the Confirm screen. Why is that?
A thunk created with createAsyncThunk will always resolve but if you want to catch it in the function that dispatches the thunk you have to use unwrapResults.
The thunks generated by createAsyncThunk will always return a resolved promise with either the fulfilled action object or rejected action object inside, as appropriate.
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an unwrapResult function that can be used to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:
import { unwrapResult } from '#reduxjs/toolkit'
// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then(originalPromiseResult => {})
.catch(rejectedValueOrSerializedError => {})
}

How to handle an action correctly with redux-saga?

I am using redux-saga in my project with react. One of my sagas looks like this:
function* deleteCitySaga(payload) {
const config = {
headers: {
Authorization: 'Bearer ' + loadState(),
Accept: 'application/json'
}
};
const data = yield axios.delete(routeDeleteCiudad + payload.id, config)
.then(response => response)
.catch(err => err.response);
}
What I want to do is to see my action as a promise, so I can know when the request made from saga is finished. This is what the call to action looks like from my component:
handleEliminar = () => {
this.props.handleDelete(this.state.id)
.then((success)=>{console.log('End')})//This is what I want to do, I want to know when my action ends
.catch((error)=>alertError);
};
const mapDispatchToProps = dispatch => ({
handleDelete: bindActionCreators(deleteCity, dispatch),
});
what would be the best way to do what I need?
Redux saga doesn't really have an easy way to do that. Most commonly, you'll fire off an action and then forget about it. If you do care about the results of that action, you can have the saga make some change to the redux store, and that state change is what alerts you to the fact that it's done.
I can think of one way to do it: When dispatching the action, you'll need to create a new promise, and pass the resolve and reject functions as part of the action. The saga will then do what it does and call resolve or reject once its done. For example:
const mapDispatchToProps = dispatch => ({
handleDelete: (id) => new Promise((resolve, reject) => {
dispatch({ type: 'deleteCity', id, resolve, reject });
});
})
function* deleteCitySaga(action) {
try {
// some code omitted
const response = yield axios.delete(routeDeleteCiudad + action.id, config);
action.resolve(response);
} catch (err) {
action.reject(err);
}
}

redux: dispatch is not working

I am developing a react app with redux
I wrote my actions in a separate js file as shown below
function getCity(city,dispatch) {
fetch('https://################', {
method: "GET",
headers: {
"user-key": "#############",
"Content-Type": "application/json",
},
}).then((res) => {
return res.json()
}).then((data) => {
console.log(data.location_suggestions);
dispatch({type:'getting_cities', city:data.location_suggestions});
})
}
then I mapped them with the code below
function mapDispatchToProps(dispatch) {
return {
getLocations:(data) => getCity(data),
logStore: () => dispatch({type:'LOGSTORE'})
}
}
console.log is working great but dispatch is not working.
please help me
It seems like the solution is simple:
You forgot to send dispatch with getCity in the snippet where you are calling your action
// Change this
getCity(data)
// To this
getCity(data, dispatch)
I recommend you to check out the redux docs about Async Actions especially the Async Action Creators section.
Try the following:
function getCity(city) {
return function (dispatch) {
return fetch(...)
.then(res => res.json())
.then(data => dispatch({ ... }));
}
}
You can also use more cool arrow syntax. This is equivalent.
function getCity(city) {
return dispatch =>
fetch(...)
.then(...)
.then(...);
}
Don't forget to export your function since it's in a separate file.
Now to dispatch your action creator in your main file:
import { getCity } from '...';
store.dispatch(getCity('berlin'));
Be sure to use connect if you work with React.

react-axios-middleware promise handling

I am working on a React application which uses redux-axios-middleware to do XHR requests. The way it is setup is with a client in the index.js like so:
const client = axios.create({ //all axios can be used, shown in axios documentation
baseURL:'https://api.vktest.xxx/api',
responseType: 'json',
validateStatus: function (status) {
return status >= 200 && status < 300
}
});
const axiosMiddle = axiosMiddleware(client)
export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk, axiosMiddle)))
From my component I am then calling my action like so:
store.dispatch(loginAction(credentials))
This is the action:
import { LOGIN } from './types'
export function loginAction(credentials) {
return dispatch => {
return dispatch(doLogin(credentials) ).then(
response => {
dispatch({type: 'LOGIN_SUCCESS', response})
},
error => {
dispatch({type: 'LOGIN_FAIL', error})
throw error
}
)
}
}
function doLogin(credentials) {
return {
type: LOGIN,
payload: {
request: {
url: '/login/',
method: 'POST',
data: {
username: credentials.username,
password: credentials.password
}
}
}
}
}
It dispatches the doLogin function which is captured by axios-middleware to do the XHR call. After the call the axios promise contains either the type LOGIN_SUCCESS or LOGIN_FAIL. Axios automatically dispatches these types and the reducers can act on them. So far so good.
However (and I am trying to be as clear as possible), when the axios request fails, the axios promise is rejected and the LOGIN_FAIL type is dispatched. Since that promise is resolved the .then(..).catch(..) block is alway's calling the .then. I cannot seem to handle the axios error with doing something ugly in the .then block which handles the dispatch callback and not the axios callback.
To clearify the following is happening:
store.dispatch -> action -> doLogin -> axios -> reducer -> back to action dispatch.
I want to handle XHR errors between the axios and reducer step but when I do:
import { LOGIN } from './types'
export function loginAction(credentials) {
return dispatch => {
return dispatch(doLogin(credentials).then(
response => {
dispatch({type: 'LOGIN_SUCCESS', response})
},
error => {
dispatch({type: 'LOGIN_FAIL', error})
throw error
}
))
}
}
function doLogin(credentials) {
return {
type: LOGIN,
payload: {
request: {
url: '/login/',
method: 'POST',
data: {
username: credentials.username,
password: credentials.password
}
}
}
}
}
I get the following error:
Uncaught TypeError: doLogin(...).then is not a function
at authentication.js:6
The reason I want to fix this is because the current setup dispatches the following:
As seen in the image both the LOGIN_FAIL (axios) and the LOGIN_SUCCESS (dispatch) are being called. I want to be able to handle the axios error myself since I want to be able to for example give the user feedback of a failed login attempt. Somehow I am not able to figure out how.
Can someone help me?
I think I know what you mean.
You can change that behavior by setting returnRejectedPromiseOnError in middleware config.
Note that promise is returned only to chain actions. If you set this option you will have to always apply catch callback or you will be getting unhandled rejection when calling Promise errors. And this might be causing some issues when handled in components, because these callbacks may be fired when component is already unmounted.
Im my code I try to avoid then as much as possible to avoid these side effects but when I have to I do it like:
fetchSomething.then(action => {
if(action.type.endsWith('SUCCESS')) {
....
}
})

React, Redux and Axios - trying to make API call

It's my first experience with React, Redux and I am totally lost. The problem is my action :
import axios from 'axios';
import { FETCH_MOVIE } from '../constants/actionTypes';
const API_KEY = <API_KEY>;
const ROOT_URL = `<API_URL>`;
export function fetchMovies(pop){
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
console.log("response is",response)
})
.catch(function (error) {
console.log(error);
});
return{
type: FETCH_MOVIE,
payload: response.data
};
}
On Console.log it seems just fine - I can see the response has the data I need. But when I am trying to send response.data to payload it returns the error - response is not defined. What am I doing wrong?
P.s. I also tried to create const result = [] and than result = [...response.data]. The error was - SyntaxError: "result" is read-only
The const error is because, result being a variable that changes over the course of the execution, you must use 'let' and not 'const'.
Now, for the fix, response is not defined comes from the last return. A good approach would be to, instead of returning the action on this function fetchMovies, you should dispatch a new action, e.g dispatch(fetchMoviesSuccess(payload)) instead of "console.log("response is",response)", which will dispatch an action that will trigger the reducer, and , in turn, update the state of the app.
You are performing async request using axios. You should dispatch your action using redux-thunk. Installation is easy, read more about thunk here.
Then your action should look like this:
export function fetchMovies(pop) {
return dispatch => {
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
console.log("response is",response);
dispatch({
type: FETCH_MOVIE,
payload: response.data
});
})
.catch(function (error) {
console.log(error);
// You can dispatch here error
// Example
dispatch({
type: FETCH_MOVIE_FAILED,
payload: error
});
});
}
}
The issue with your code is that by the time you return, response is still undefined because this code run synchronously till the return statement.
As you can see response is defined in console.log("response is",response)
So this is where you need to do your actual magic return but in another way.
You can use redux-thunk to do these thing because this is redux async. but as I feel you are a beginner from the code I have seen, Just use the simpler way and read redux-thunk or redux-promise. if you feel your project needs this then go one.
//try to make the caller pass this.props.dispatch as param
export function fetchMovies(dispatch, pop){
const url = `${ROOT_URL}?api_key=${API_KEY}&sort_by=${pop}`;
axios.get(url)
.then(function (response) {
// only here is response define so use dispatch to triger another action (fetched data with response)
dispatch({
type: FETCH_MOVIE,
payload: response.data
})
})
.catch(function (error) {
//if you had a loader state, you call also toggle that here with erro status
console.log(error);
});
}
//now on the caller (onClick for instance) do this instead
fetchMovies(this.props.dispatch, pop)
As you can see from #loelsonk answer down. if you use redux-thunk then you won't need to pass dispatch from the caller redux-thunk for you. But also notice how you would return and anonymous arrow function which accept dispatch as a parameter.
You can use redux promise middleware. I have used this in my new project. It is very simple and keeps our code and state manageable.
For every async action dispatch, it dispatches
$action_type_PENDING immediately after our action dispatch , $action_type_FULFILLED if api call success, $action_type_REJECTED if api call failure
See documentation- https://github.com/pburtchaell/redux-promise-middleware
Example from my project-
your action is
export function getQuestions() {
return {
type: types.GET_QUESTIONS,
payload: axios.get('http://localhost:3001/questions')
};
}
reducer is
const initialState = {
isLoading: false,
questions: []
};
const questions = (state = initialState.questions, action) => {
switch(action.type) {
case types.GET_QUESTIONS_FULFILLED:
return [...action.payload.data];
default: return state;
}
};
For displaying loader while api call we can use following reducer
const isLoading = (state = initialState.isLoading, action) => {
switch(action.type) {
case (action.type.match(/_PENDING/) || {}).input:
return true;
case (action.type.match(/_FULFILLED/) || {}).input:
return false;
default: return state;
}
};
Comment me if you need any more details on above stuff.

Resources