This is stemming off of this SO Question
I am trying to integrate redux-thunk with my API middleware. The current flow of logic is like this:
action dispatched from component this.props.onVerifyLogin();
=>
action goes to action creator which creates an API call to the middleware like so:
// imports
const verifyLoginAC = createAction(API, apiPayloadCreator);
export const verifyLogin = () => dispatch => {
return verifyLoginAC({
url: "/verify/",
method: "POST",
data: {
token: `${
localStorage.getItem("token")
? localStorage.getItem("token")
: "not_valid_token"
}`
},
onSuccess: result => verifiedLogin(dispatch, result),
onFailure: result => dispatch(failedLogin(result))
});
};
const verifiedLogin = (dispatch, data) => {
console.log("verifiedLogin");
const user = {
...data.user
};
dispatch(setUser(user));
dispatch({
type: IS_LOGGED_IN,
payload: true
});
};
// failedLogin function
const setUser = createAction(SET_USER);
apiPayloadCreator in utils/appUtils:
const noOp = () => ({ type: "NO_OP" });
export const apiPayloadCreator = ({
url = "/",
method = "GET",
onSuccess = noOp,
onFailure = noOp,
label = "",
isAuthenticated = false,
data = null
}) => {
return {
url,
method,
onSuccess,
onFailure,
isAuthenticated,
data,
label
};
};
and then the middleware intercepts and performs the actual API call:
// imports
// axios default config
const api = ({ dispatch }) => next => action => {
next(action);
console.log("IN API");
console.log("Action: ", action);
// this is where I suspect it is failing. It expects an action object
// but is receiving a function (see below for console.log output)
if (action.type !== API) return;
// handle Axios, fire onSuccess/onFailure, etc
The action is created but is a function instead of an action creator (I understand this is intended for redux-thunk). But when my API goes to check action.type it is not API so it returns, never actually doing anything including call the onSuccess function. I have tried to also add redux-thunk before api in the applyMiddleware but then none of my API actions fire. Can someone assist?
Edit:
This is the received data to the API middleware:
ƒ (dispatch) {
return verifyLoginAC({
url: "/verify/",
method: "POST",
data: {
token: "" + (localStorage.getItem("token") ? localStorage.getItem("token") : "not_valid_toke…
Status Update:
Still unable to get it work properly. It seems like redux-saga has a pretty good following also, should I try that instead?
My API was interferring. I switched to redux-saga and got everything working like so:
/**
* Redux-saga generator that watches for an action of type
* VERIFY_LOGIN, and then runs the verifyLogin generator
*/
export function* watchVerifyLogin() {
yield takeEvery(VERIFY_LOGIN, verifyLogin);
}
/**
* Redux-saga generator that is called by watchVerifyLogin and queries the
* api to verify that the current token in localStorage is still valid.
* IF SO: SET loggedIn = true, and user = response.data.user
* IF NOT: SET loggedIn = false, and user = {} (blank object}
*/
export function* verifyLogin() {
try {
apiStart(VERIFY_LOGIN);
const token = yield select(selectToken);
const response = yield call(axios.post, "/verify/", {
// use redux-saga's select method to select the token from the state
token: token
});
yield put(setUser(response.data.user));
yield put(setLoggedIn(true));
apiEnd(VERIFY_LOGIN);
} catch (error) {
apiEnd(VERIFY_LOGIN);
yield put(setLoggedIn(false));
yield put(setUser({})); // SET USER TO BLANK OBJECT
}
}
Related
I have 3 generator function first is "loginUserStart" where the actual request comes then the second one is "LoginUserAsync" which is called in the "loginUserStart" and third is api call function
so I am trying to pass the parameter from my signin component to the loginUserStart function but whenever I console.log(arguments) it is showing nothing
Code:-
Sign-in component
const login = async () => {
arr.userEmail = "sample_email";
arr.userPassword = "sample_password";
console.log(arr);
signinUserStart(arr);
};
const logSubmit = () => {
login();
};
const mapDispatchToProps = (dispatch) => ({
signinUserStart: (data) => dispatch(signinUserStart(data))
});
Action file code
export const signinUserStart = (data) => ({
type: UserActionTypes.Set_SigninUser_Start,
payload: data
})
saga File code
API generator function code
export async function fetchUser(info) {
console.log(info);
const email = 'Admin#gmail.com'; //sample_email
// const passwords = info.userPassword;
const password = 'Admin#123'; //sample_password
try {
const user = await axios.post("http://localhost:5050/sign", {
data: {
email: email,
password: password,
},
});
console.log(user);
return user;
} catch (error) {
console.log(error);
return error;
}
}
LoginUserAsync function
export function* LoginUserAsync(data) {
console.log("in saga");
console.log(data);
try {
let userInfo = yield call(fetchUser, data)
console.log(userInfo);
yield put(setUserId('62b1c5ee515317d42239066a')); //sample_token
yield put(setCurrentUserName(userInfo.data.userName));
} catch (err) {
console.log(err);
}
}
loginUserStart function
export function* loginUserStart(action) {
console.log(action.payload);//not logging anything for showing in console
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync(action));
}
I can't be sure without seeing more code, but assuming that loginUserStart is either root saga or started from root saga it means there is no action for it to receive.
The main issue I think is this line
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync(action));
In the second parameter you are calling the generator function which is wrong, instead you should be passing the saga itself (as reference).
So it should look like this:
yield takeLatest(UserActionTypes.Set_SigninUser_Start, LoginUserAsync);
This way, the Redux Saga library will then call LoginUserAsync when Set_SigninUser_Start is dispatched with first param correctly set to the action object.
I'm fairly new to redux toolkit so I'm still having a few issues with it!
As per the code below, I'm trying to access state (loginDetails.username and loginDetails.password) inside my createAsyncThunk. I'm obviously doing something wrong here - I've tried writing the createAsyncThunk function inside a different file, attempting to access the state inside that file and then importing the function, but either way it's failing.
// Import: Packages
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
// AsyncThunk: getUserDetails
export const getUserDetails = createAsyncThunk(
"userDetails/getUserDetails",
async () => {
try {
const apiUrl = process.env.REACT_APP_URL;
var config = {
method: "get",
url: `${apiUrl}/claimSet?UserName=${state.loginDetails.username}&Password=${state.loginDetails.password}`,
headers: {
accept: "application/json",
},
};
const response = await axios(config);
const data = await response.data;
return data;
} catch (error) {
console.log(error);
}
}
);
// Slice: userDetailsSlice
export const userDetailsSlice = createSlice({
name: "userDetails",
initialState: {
loginDetails: {
username: "",
password: "",
},
details: [],
status: null,
},
reducers: {
addUsername: (state, { payload }) => {
state.loginDetails.username = payload;
},
addPassword: (state, { payload }) => {
state.loginDetails.password = payload;
},
},
extraReducers: {
[getUserDetails.pending]: (state, action) => {
state.status = "loading";
},
[getUserDetails.fulfilled]: (state, { payload }) => {
state.details = payload;
state.status = "success";
},
[getUserDetails.rejected]: (state, action) => {
state.status = "failed";
},
},
});
// Actions: addUsername, addPassword
export const { addUsername, addPassword } = userDetailsSlice.actions;
// Reducer: userDetailsSlice.reducer
export default userDetailsSlice.reducer;
The code in the config url ${state.loginDetails.username}, etc. is just one of many failed attempts to get hold of the state. I understand that part of the issue is that the createAsyncThunk is declared before the state/slide is below, but I still can't seem to find a way around it.
Any help would be really appreciated!
Thanks in advance <3
The async function consumes a "payload" argument, and secondly a thunkAPI object that contains a getState method.
payloadCreator
thunkAPI: an object containing all of the parameters that are normally
passed to a Redux thunk function, as well as additional options:
dispatch: the Redux store dispatch method
getState: the Redux store getState method
extra: the "extra argument" given to the thunk middleware on setup, if available
requestId: a unique string ID value that was automatically generated to identify this request sequence
signal: an AbortController.signal object that may be used to see if another part of the app logic has marked this request as needing
cancelation.
rejectWithValue: rejectWithValue is a utility function that you can return in your action creator to return a rejected response with a
defined payload. It will pass whatever value you give it and return it
in the payload of the rejected action.
// AsyncThunk: getUserDetails
export const getUserDetails = createAsyncThunk(
"userDetails/getUserDetails",
async (arg, { getState }) => { // <-- destructure getState method
const state = getState(); // <-- invoke and access state object
try {
const apiUrl = process.env.REACT_APP_URL;
var config = {
method: "get",
url: `${apiUrl}/claimSet?UserName=${state.loginDetails.username}&Password=${state.loginDetails.password}`,
headers: {
accept: "application/json",
},
};
const response = await axios(config);
const data = await response.data;
return data;
} catch (error) {
console.log(error);
}
}
);
I am using redux and redux-saga in my project. Right now using WebSocket I have a problem calling a FETCH_SUCCESS redux action inside a callback of socket response. I tried making the callback a generator as well but didn't work as well.
function* websocketSaga() {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
const token = yield select(selectToken);
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
console.log(response); // here is the proper response, it works
put({
type: FETCH_SUCCESS, // here the FETCH_SUCCESS action is not called
payload: response.dataResponse,
});
});
...
....
}
);
}
Or maybe this WebSocket should be implemented in a completely different way in redux-saga?
You won't be able to use yield put inside a callback function. Stompjs knows nothing about sagas, so it doesn't know what it's supposed to do when given a generator function.
The simplest approach, though not necessarily the best, is to go directly to the redux store in the callback, and dispatch the action without involving redux-saga. For example:
import store from 'wherever you setup your store'
// ...
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
store.dispatch({
type: FETCH_SUCCESS,
payload: response.dataResponse,
});
});
If you'd like to use a more redux-saga-y approach, I would recommend wrapping the subscription in an event channel. Event channels take a callback-based API and turn it into something that you can interact with using redux-saga's effects such as take
Here's how you might create the event channel:
import { eventChannel } from 'redux-saga';
function createChannel(token) {
return eventChannel(emitter => {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
emitter(response); // This is the value which will be made available to your saga
});
}
);
// Returning a cleanup function, to be called if the saga completes or is cancelled
return () => stomp.disconnect();
});
}
And then you'd use it like this:
function* websocketSaga() {
const token = yield select(selectToken);
const channel = createChannel(token);
while (true) {
const response = yield take(channel);
yield put({
type: FETCH_SUCCESS,
payload: response.dataResponse,
});
}
}
Promise should be the perfect fit. Just wrap the callback related code in a promise and resolve it in the callback function. After that use the yield to get the data from the promise. I have modified your code with the Promise below.
function* websocketSaga() {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
const token = yield select(selectToken);
const p = new Promise((resolve, reject) => {
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
console.log(response); // here is the proper response, it works
resolve(response); // here resolve the promise, or reject if any error
});
...
....
}
);
});
try {
const response = yield p; // here you will get the resolved data
yield put({
type: FETCH_SUCCESS, // here the FETCH_SUCCESS action is not called
payload: response.dataResponse,
});
} catch (ex) {
// handle error here, with rejected value
}
}
I will give you another way of managing this: create a component connected to redux where you will handle the WS subscription. This component will not render anything to the UI but will be useful for handling redux store interactions.
The main idea is, don't put everything into redux-saga, try and split it into multiple parts to make it easier to maintain.
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
function WSConnection(props) {
const {token, fetchDone} = props;
const [stomp, setStomp] = React.useState();
const onMessage = React.useCallback(message => {
const response = JSON.parse(message.body);
fetchDone(response.dataResponse);
}, [fetchDone]);
const onConnect = React.useCallback(frame => {
const subscription = stomp.subscribe('/queue/data', onMessage);
// cleanup subscription
return () => subscription.unsubscribe();
}, [stomp, onMessage]);
const onError = React.useCallback(error => {
// some error happened, handle it here
}, []);
React.useEffect(() => {
const header = {Authorization: `Bearer ${token}`};
stomp.connect(header, onConnect, onError);
// cleanup function
return () => stomp.disconnect();
}, [stomp])
React.useEffect(() => {
setStomp(Stomp.over(socket));
}, []);
return null;
}
const mapStateToProps = state => ({
... // whatever you need from redux store
});
const mapDispatchToProps = dispatch => ({
... // whatever actions you need to dispatch
});
export default connect(mapStateToProps, mapDispatchToProps)(WSConnection);
You can also take it a step further and extract the stomp logic into another file and reuse it wherever you will need it.
It's not wrong to put everything into redux-saga but it's a nice alternative to handle WS connections inside components connected to redux (and easier to understand to people who are not completely familiar with redux-saga and channels etc).
I have the same stack over the years and only recently I faced websockets over Stomp client.
None of the above solutions doesn't work for me both technically and mentally
Reasons:
I don't like channels with Stomp because the only way to manipulate connections in more surgical way you have to use global state object (for me - it's redux). It doesn't seems right even if you storing only random generated IDS (with unsubscribe function it will be... read more here about store serialization
the way with container another pain in the ... (you know where). Again redux and a lot of under-the-hood functionality used without any reason
another way with promises: again without storing helpful connection info and some DI by using promises inside generators. This narrows down the implementation choice
So:
I need to have connection info (I decided to use state but not in: redux, component state. Singleton state). Stomp doesn't force you to place ID but I do because I want to manage connections by myself
I need one entry point without: promises, iterators and a lot of things that will be pain for future-me. One place to "rule them all" (as I want)
- activate: login
- deactivate: logout
- subscribe: componentDidMount
- unsubscribe: componentWillUnmount
DI by request in one place (passing store.dispatch to constructor only if need it) // main topic of the question
And I wrote this implementation that perfectly works for me:
import SockJS from 'sockjs-client';
import {
Client,
IMessage,
messageCallbackType,
StompHeaders,
} from '#stomp/stompjs';
import { Action, Dispatch } from 'redux';
type ConnectionId = string;
interface IServiceConfig {
url: string;
dispatch?: Dispatch;
}
export default class Stomp {
serviceConfig: IServiceConfig = {
dispatch: null,
url: null,
};
ids: ConnectionId[] = [];
stomp: Client;
constructor(config: IServiceConfig) {
this.serviceConfig = { ...config };
this.stomp = new Client();
this.stomp.webSocketFactory = () => {
return (new SockJS(config.url));
};
}
alreadyInQueue = (id: ConnectionId): boolean => {
return Boolean(this.ids.find(_id => id === _id));
};
subscribeByDispatchAction = (
destination: string,
callback: (message: IMessage) => Action,
headers: StompHeaders & {
id: ConnectionId;
},
): void => {
const alreadyInQueue = this.alreadyInQueue(headers.id);
if (!alreadyInQueue) {
this.stomp.subscribe(
destination,
(message) => {
this.serviceConfig.dispatch(callback(message));
},
headers,
);
this.ids.push(headers.id);
return;
}
console.warn(`Already in queue #${headers.id}`);
};
subscribe = (
destination: string,
callback: messageCallbackType,
headers: StompHeaders & {
id: ConnectionId;
},
): void => {
const alreadyInQueue = this.alreadyInQueue(headers.id);
if (!alreadyInQueue) {
this.stomp.subscribe(
destination,
(message) => callback(message),
headers,
);
this.ids.push(headers.id);
this.logState('subscribe');
return;
}
console.warn(`Failed to subscribe over Socks by #${headers.id}`);
};
unsubscribe = (id: ConnectionId, headers?: StompHeaders): void => {
this.stomp.unsubscribe(id, headers);
this.ids.splice(this.ids.indexOf(id), 1);
};
activate = (): void => {
this.stomp.activate();
};
deactivate = (): void => {
if (this.ids.length === 0) {
this.stomp.deactivate();
return;
}
for (let i = 0; i < this.ids.length; i++) {
this.unsubscribe(this.ids[i]);
}
/**
* it seems like it's overkil but
* for me it works only if i do all
* the things as you see below
* - stomp deactivation
* - closing webSockets manually by using native constant // sockjs-client
* - closing webSockets instance by using returned value fron factory
*/
this.stomp.deactivate();
this.stomp.webSocket.close(
this.stomp.webSocket.CLOSED,
);
this.stomp.webSocketFactory().close();
};
getAllIds = (): readonly ConnectionId[] => {
return this.ids;
};
// debug method
logState = (method: string): void => {
/* eslint-disable */
console.group(`Stomp.${method}`);
console.log('this', this);
console.log('this.ids', this.getAllIds());
console.log('this.stomp', this.stomp);
console.groupEnd();
/* eslint-enable */
};
}
My configuration file
import { store } from '~/index';
import Stomp from '~/modules/_Core/services/Stomp';
import appConfig from '~/modules/Common/services/appConfig';
export const StompService = new Stomp({
dispatch: store?.dispatch,
url: `${appConfig.apiV1}/websocket`,
});
I hope that it will help someone
Last few days I tried to write some middleware that checks wether the token stored in the redux-store is still valid and not reached it's expiry date. If it is not valid anymore it should refresh the token before executing any other async call. The problem I am encountering right now is that the async redux functions in the components are called first before the middleware is being called.
Currently I wrote the following middleware:
reduxMiddleware.js
const refreshJwt = ({ dispatch, getState }) => {
return (next) => (action) => {
console.log(typeof action);
if (typeof action === "function") {
if (getState().authentication.token) {
// decode jwt so that we know if and when it expires
var tokenExpiration = parseJwt(getState().authentication.token).exp;
if (
tokenExpiration &&
moment(tokenExpiration) <
moment(Math.floor(Date.now().valueOf() / 1000))._i
) {
console.log("start refreshing");
startRefreshToken(getState().authentication.refreshToken).then(
(token) => {
console.log("done refreshing");
dispatch(updateAccessToken(token));
next(action);
}
);
}
}
}
return next(action);
};
};
export default refreshJwt;
I apply this middleware like so:
export default () => {
const store = createStore(
combineReducers({
authentication: authenticationReducer,
venue: venueReducer,
tables: tableReducer
}),
composeEnhancers(applyMiddleware(refreshJwt, thunk))
);
return store;
};
The startRefreshToken code is:
const startRefreshToken = (refresh_token) => {
return httpPost(
process.env.NODE_ENV
? `https://tabbs-api.herokuapp.com/api/v1/token`
: `http://localhost:3000/api/v1/token`,
{
refresh_token
}
)
.then((response) => {
localStorage.setItem(
"authentication",
JSON.stringify({
token: response.data.token,
refreshToken: refresh_token
})
);
return response.data.token;
})
.catch((error) => {
return Promise.reject(error.response);
});
};
Order of calling:
Legend:
Executing call now stands for the function being called in the component
start refreshing stands for the middleware being called
Currently I am experiencing the following issue:
When a async function in the components didComponentMount is being called, it will be called before the middleware function is being called. This is causing that it will be using the old token stored in the redux/local storage.
I really can't find the issue till today and would like to get some external help for this issue.
I am aware that this is duplicate of :
How to use Redux to refresh JWT token?
Thanks for the help. If you'll need additional context / code please do not hesitate to comment. I'll add it to codesandbox.
Best Kevin.
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())
}