Redux Saga performs yield multiple times - react-redux

I'm fighting with strange behaviour of my saga.
The story is simple. User clicks the button and dispatch 'generateUserPassword' action.
My saga takes this action and should call password generation only once, but does it multiple times. I do not have backend for that, so i get 404 http response and catch an exception in saga. Then, generateUserPasswordFailed action is dispatched and handled in handleGenerationFailed()
function* generateUserPassword(action: GenerateUserPassword): unknown {
const request: PasswordGenerateRequest = { email: action.email }
try {
yield callPasswordGenerate(request)
yield put(userPasswordGenerated())
} catch (e) {
yield put(generateUserPasswordFailed())
}
}
function* handlePasswordGeneration(): unknown {
yield put(showInfoMessage('Hasło zostało wysłane do Ciebie smsem'))
}
function* handleGenerationFailed(): unknown {
yield put(showWarningAlert('Coś poszło nie tak. Spróbuj ponownie'))
}
export function* userPasswordSaga(): IterableIterator<ForkEffect> {
yield takeLatest(GENERATE_USER_PASSWORD, generateUserPassword)
yield takeLatest(GENERATE_USER_PASSWORD_FAILED, handleGenerationFailed)
yield takeLatest(USER_PASSWORD_GENERATED, handlePasswordGeneration)
}
In result, in console i see this:
As you see, DISPATCH_ALERT is called multiple times.
And 783 requests have been sent:
But why 783?
These are my actions:
export type GenerateUserPassword = {
type: typeof GENERATE_USER_PASSWORD
email: string
}
export type UserPasswordGenerated = {
type: typeof USER_PASSWORD_GENERATED
}
export type GenerateUserPasswordFailed = {
type: typeof GENERATE_USER_PASSWORD_FAILED
}
export const showWarningAlert = (alertKey: string, prefixKey?: string): DispatchAlert => ({
type: DISPATCH_ALERT,
alert: {
type: 'WARNING',
key: alertKey,
prefixKey
}
})
And here is http call:
export type PasswordGenerateRequest = {
email: string
}
export const callPasswordGenerate = async (request: PasswordGenerateRequest): Promise<unknown> => {
const response = await axios.post(`/passCreate`, request)
return response.data
}
It gets weirder, when I put console.log in my saga. It is called 782 times and i don't see any further actions dispatched.
function* generateUserPassword(action: GenerateUserPassword): unknown {
const request: PasswordGenerateRequest = { email: action.email }
console.log('generate user password called')
try {
yield callPasswordGenerate(request)
yield put(userPasswordGenerated())
} catch (e) {
yield put(generateUserPasswordFailed())
}
}
When i put console.log in catch clause:
function* generateUserPassword(action: GenerateUserPassword): unknown {
const request: PasswordGenerateRequest = { email: action.email }
try {
yield callPasswordGenerate(request)
yield put(userPasswordGenerated())
} catch (e) {
console.log('exception')
yield put(generateUserPasswordFailed())
}
}
It's logged only once:
Please help me to understand this.
Thanks in advance!

Related

Providing two combined Reducers for my redux saga store prevents my websocket channel message from triggering, but only one does not?

Configured my store this way with redux toolkit for sure
const rootReducer = combineReducers({
someReducer,
systemsConfigs
});
const store = return configureStore({
devTools: true,
reducer: rootReducer ,
// middleware: [middleware, logger],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ thunk: false }).concat(middleware),
});
middleware.run(sagaRoot)
And thats my channel i am connecting to it
export function createSocketChannel(
productId: ProductId,
pair: string,
createSocket = () => new WebSocket('wss://somewebsocket')
) {
return eventChannel<SocketEvent>((emitter) => {
const socket_OrderBook = createSocket();
socket_OrderBook.addEventListener('open', () => {
emitter({
type: 'connection-established',
payload: true,
});
socket_OrderBook.send(
`subscribe-asdqwe`
);
});
socket_OrderBook.addEventListener('message', (event) => {
if (event.data?.includes('bids')) {
emitter({
type: 'message',
payload: JSON.parse(event.data),
});
//
}
});
socket_OrderBook.addEventListener('close', (event: any) => {
emitter(new SocketClosedByServer());
});
return () => {
if (socket_OrderBook.readyState === WebSocket.OPEN) {
socket_OrderBook.send(
`unsubscribe-order-book-${pair}`
);
}
if (socket_OrderBook.readyState === WebSocket.OPEN || socket_OrderBook.readyState === WebSocket.CONNECTING) {
socket_OrderBook.close();
}
};
}, buffers.expanding<SocketEvent>());
}
And here's how my saga connecting handlers looks like
export function* handleConnectingSocket(ctx: SagaContext) {
try {
const productId = yield select((state: State) => state.productId);
const requested_pair = yield select((state: State) => state.requested_pair);
if (ctx.socketChannel === null) {
ctx.socketChannel = yield call(createSocketChannel, productId, requested_pair);
}
//
const message: SocketEvent = yield take(ctx.socketChannel!);
if (message.type !== 'connection-established') {
throw new SocketUnexpectedResponseError();
}
yield put(connectedSocket());
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.BAD_CONNECTION,
})
);
}
}
export function* handleConnectedSocket(ctx: SagaContext) {
try {
while (true) {
if (ctx.socketChannel === null) {
break;
}
const events = yield flush(ctx.socketChannel);
const startedExecutingAt = performance.now();
if (Array.isArray(events)) {
const deltas = events.reduce(
(patch, event) => {
if (event.type === 'message') {
patch.bids.push(...event.payload.data?.bids);
patch.asks.push(...event.payload.data?.asks);
//
}
//
return patch;
},
{ bids: [], asks: [] } as SocketMessage
);
if (deltas.bids.length || deltas.asks.length) {
yield putResolve(receivedDeltas(deltas));
}
}
yield call(delayNextDispatch, startedExecutingAt);
}
} catch (error: any) {
reportError(error);
yield put(
disconnectedSocket({
reason: SocketStateReasons.UNKNOWN,
})
);
}
}
After Debugging I got the following:
The Thing is that when I Provide one Reducer to my store the channel works well and data is fetched where as when providing combinedReducers I am getting
an established connection from my handleConnectingSocket generator function
and an empty event array [] from
const events = yield flush(ctx.socketChannel) written in handleConnectedSocket
Tried to clarify as much as possible
ok so I start refactoring my typescript by changing the types, then saw all the places that break, there was a problem in my sagas.tsx.
Ping me if someone faced such an issue in the future

Redux saga failed in production

I've been trying to tackle this problem more than 2 weeks now. Everything works fine in development mode. But not in production mode. The example below are shown using Redux Saga environment (I'm still new in redux saga). But I've tried re-do it using Context API. Unfortunately the problem still persists. (below are the images showing successful process in development mode & unsuccessful process in production mode)
successful in development mode
unsuccessful in production mode
My guess it could be something to do with status code 304 Not Modified. Since the data I tried to fetch not changing, thus it will use cached data in browser. But I don't know how to setup my server so that I can handle this issue. I have read a bunch of online threads. But none were able to resolve my issue.
You may have a look at my code right now. Bear in mind that everything works just fine in development mode. From the images above you can see that I don't have problem logging in. Just fetching & getting data to be displayed in dashboard got error.
client/src/redux/actions/Dashboard.js (Action)
import { SET_ISDASHBOARD, SET_LOADING, SET_ERROR } from '../sagas/types'
// Set Loading
export const setLoading = (status) => ({
type: SET_LOADING,
payload: status
})
// Set Error
export const setError = (error) => ({
type: SET_ERROR,
payload: { error: error.status, message: error.message }
})
// Dashboard
export const isDashboard = () => ({
type: SET_ISDASHBOARD
})
client/src/redux/reducers/Dashboard.jd (Reducer)
import { SET_ERROR, SET_LOADING, SET_ISDASHBOARD, SET_DASHBOARD } from '../sagas/types'
const initialState = {
user: {
id: '',
name: '',
email: ''
},
loading: false,
error: false,
message: ''
}
const reducer = (state = initialState, action) => {
switch(action.type) {
case SET_ISDASHBOARD:
return {
...state,
loading: true
}
case SET_DASHBOARD:
return {
...state,
user: {
...state.user,
id: action.payload.id,
email: action.payload.email,
name: action.payload.name
}
}
case SET_ERROR:
return {
...state,
error: action.payload.status,
message: action.payload.message
}
case SET_LOADING:
return {
...state,
loading: action.payload
}
default:
return state
}
}
export default reducer
client/src/redux/sagas/handlers/dashboard.js (Saga handlers)
import { call, put } from 'redux-saga/effects'
import { requestGetDashboard } from '../requests/dashboard'
import { SET_LOADING, SET_ERROR, SET_DASHBOARD } from '../types'
export function* handleGetDashboard(action) {
try {
const response = yield call(requestGetDashboard)
const result = response.data.data
console.log(response); console.log(result)
// dispatch set dashboard
yield put({ type: SET_DASHBOARD, payload: { id: result.id, email: result.email, name: result.name } })
} catch(error) {
// console.log(error); console.log(error.response)
const result = error.response.data
const payload = {
status: true,
message: result.error
}
// dispatch setError
yield put({ type: SET_ERROR, payload: payload })
}
// loading to false
yield put({ type: SET_LOADING, payload: false })
}
client/src/redux/sagas/requests/dashboard.js (Saga requests)
import axios from 'axios'
/** get dashboard */
export const requestGetDashboard = () => {
return axios.get(
'/api/v1/dashboard',
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('uid')}`
}
}
)
}
client/src/redux/sagas/rootSaga.js (Root Saga)
import {
SET_ISLOGIN, SET_ISLOGOUT, SET_ISAUTH,
SET_ISDASHBOARD,
} from './types'
import { takeLatest } from 'redux-saga/effects'
import { handleClientAuth, handlePostLogin, handlePostLogout } from './handlers/auth'
import { handleGetDashboard } from './handlers/dashboard'
export function* watcherSaga() {
// auth
yield takeLatest(SET_ISLOGIN, handlePostLogin)
yield takeLatest(SET_ISLOGOUT, handlePostLogout)
yield takeLatest(SET_ISAUTH, handleClientAuth)
// dashboard
yield takeLatest(SET_ISDASHBOARD, handleGetDashboard)
}
client/src/redux/sagas/types.js (Types)
/** for AUTH */
export const SET_ISLOGIN = 'SET_ISLOGIN'
export const SET_ISLOGOUT = 'SET_ISLOGOUT'
export const SET_ISAUTH = 'SET_ISAUTH'
export const SET_AUTH = 'SET_AUTH'
export const SET_LOADING = 'SET_LOADING'
export const SET_ERROR = 'SET_ERROR'
/** for DASHBOARD */
export const SET_ISDASHBOARD = 'SET_ISDASHBOARD'
export const SET_DASHBOARD = 'SET_DASHBOARD'
Please point me to any directions that could help get closer insight to this problem.

Call redux action in axios interceptor

I store logged in user data in localstorage. I also validate JWT token in axios interceptor and if it's expired I will refresh it. so I need to update store with new user data and JWT token and in order to do that, I need to call redux action that I have in my Auth module.
AuthRedux.js
export const actionTypes = {
Login: "[Login] Action",
Logout: "[Logout] Action",
UserRequested: "[Request User] Action",
UserLoaded: "[Load User] Auth API",
SetUser: "[Set User] Action",
};
const initialAuthState = {
user: undefined,
authToken: undefined,
};
export const reducer = persistReducer(
{ storage, key: "myapp-auth", whitelist: ["user", "authToken"] },
(state = initialAuthState, action) => {
switch (action.type) {
case actionTypes.Login: {
const { authToken } = action.payload;
return { authToken, user: undefined };
}
case actionTypes.Logout: {
// TODO: Change this code. Actions in reducer aren't allowed.
return initialAuthState;
}
case actionTypes.UserLoaded: {
const { user } = action.payload;
return { ...state, user };
}
case actionTypes.SetUser: {
const { user } = action.payload;
return { ...state, user };
}
default:
return state;
}
}
);
export const actions = {
login: (authToken) => ({ type: actionTypes.Login, payload: { authToken } }),
logout: () => ({ type: actionTypes.Logout }),
requestUser: (user) => ({ type: actionTypes.UserRequested, payload: { user } }),
fulfillUser: (user) => ({ type: actionTypes.UserLoaded, payload: { user } }),
setUser: (user) => ({ type: actionTypes.SetUser, payload: { user } }),
};
export function* saga() {
yield takeLatest(actionTypes.Login, function* loginSaga() {
yield put(actions.requestUser());
});
yield takeLatest(actionTypes.UserRequested, function* userRequested() {
const { data: user } = yield getUserByToken();
yield put(actions.fulfillUser(user));
});
}
AxiosInterceptor.js
export default function setupAxios(axios, store, props) {
axios.interceptors.request.use(
config => {
const {
auth: { authToken }
} = store.getState();
if (authToken) {
config.headers.Authorization = `Bearer ${authToken}`;
}
return config;
},
err => Promise.reject(err)
);
axios.interceptors.response.use(
(response) => {
console.log(props);
return response;
},
function (error) {
const originalRequest = error.config;
if (error.response?.status === 401) {
if (error.response.data === "refresh_token") {
// refresh token and set new user data
// question is how can I call redux setUser action in here and update state
}
else if (error.response.data === "invalid_token") {
window.localStorage.clear();
window.location.href = '/auth/login';
}
else { }
}
if (!originalRequest._retry) {
originalRequest._retry = true;
return axios(originalRequest);
}
return Promise.reject(error);
}
);
}
My question is how can I call reducer setUser action in interceptor and update state
You can dispatch actions from outside of a component when you have access to the store with store.dispatch(anAction), in your case you can do:
store.dispatch(setUser())

Set the sequence of execution of asynchronous dispatch (redux\redux-saga)

I need to perform an asynchronous request. To do this, I'm debating a preloader, then I make a request, then I want to stop the preloader. The console should have: "show loader, load-app, hide loader", and output "show loader, hide loader, loading-app". How to save a sequence of calls?
How set the sequence of execution of asynchronous dispatch (redux\redux-saga)?
import { showLoader, hideLoader } from '../../reducer1'
import { authorizeToken } from '../reducer2'
async componentDidMount() {
const { dispatch } = this.props
const tokenLS = localStorage.getItem('token')
await dispatch(showLoader()); //show loader
await dispatch(authorizeToken(tokenLS)); // async request
await dispatch(hideLoader()); //hide loader
}
}
This code for Loader
import * as act from './actions'
const initialState = {
loadingPage: false
}
export const showLoader = () => {
console.log('show loader')
document.body.classList.add('loading-app')
return { type: act.startLoading }
}
export const hideLoader = () => {
console.log('hide loader')
document.body.classList.remove('loading-app')
return { type: act.finishLoading }
}
export default function loading(state = initialState, action) {
switch (action.type) {
case act.startLoading:
return { ...state, loadingPage: true }
case act.finishLoading:
return { ...state, loadingPage: false }
default:
return state
}
}
This code for async request:
function* authorizeWithToken({ payload: { token } }) {
try {
const { token:userToken } = yield call(authApi.authUserFromToken, token)
yield put({ type: AUTH_SUCCESS, payload: { token: userToken } })
yield console.log('end request')
} catch (error) {
throw new Error(`error request with token ${token}`)
}
}
export function* authorizeSaga() {
yield takeLatest(AUTH_REQUEST, authorize)
}
export function* authorizeWithTokenSaga() {
yield takeLatest(AUTH_REQUEST_TOKEN, authorizeWithToken)
}
This is reducer:
export const authorizeToken = (token) => ({
type: AUTH_REQUEST_TOKEN,
payload: {token}
})
I think there's some misunderstanding here. Your componentDidMount function does not need to by async. redux-saga enables you to move all async actions into sagas and out of the components. That way your components are easier to manage. I would change your componentDidMount to dispatch a single action and let your saga handle all the logic. Try changing it to this:
import { authorizeToken } from '../reducer2'
componentDidMount() {
const { dispatch } = this.props
const tokenLS = localStorage.getItem('token')
dispatch(authorizeToken(tokenLS)); // async request
}
Now in your saga, try this:
import { showLoader, hideLoader } from '../../reducer1'
function* authorizeWithToken({ payload: { token } }) {
try {
yield put(showLoader());
const { token:userToken } = yield call(authApi.authUserFromToken, token)
yield put({ type: AUTH_SUCCESS, payload: { token: userToken } })
yield put(hideLoader())
} catch (error) {
throw new Error(`error request with token ${token}`)
}
}
Now your saga will be in charge of displaying the loader, fetching the data, and hiding the loader. In that order.

Error TypeError: Cannot read property 'dispatch' of undefined at app.js:12012

Hi I've been trying to learn vuejs and vuex while trying to get response from an api call with vuex concept I got the following error.Please help.
This error occurred
Error TypeError: Cannot read property 'dispatch' of undefined
at app.js:12012
loginAction.js
export const getUsersList = function (store) {
let url = '/Apis/allUsers';
Vue.http.get(url).then((response) => {
store.dispatch('GET_USER_RES', response.data);
if (response.status == 200) {
}
}).catch((response) => {
console.log('Error', response)
})
}
loginStore.js
const state = {
userResponse: []
}
const mutations = {
GET_USER_RES (state, userResponse) {
state.userResponse = userResponse;
}
}
export default {
state, mutations
}
login.vue
import {getUsersList} from './loginAction';
export default {
created () {
try{
getUsersList();
}catch(e){
console.log(e);
}
},
vuex: {
getters: {
getUsersList: state => state.userResponse
},
actions: {
getUsersList
}
}
}
</ script>
If you call the actions manually (like in your try/catch) they'll not get the store context as the first argument. You could use getUsersList(this.store) I think, but instead I would use dispatch to reach all your actions. (I edited just a little bit to get a minimal running example, but I think you get the point!)
new Vue({
render: h => h(App),
created() {
this.$store.dispatch('getUsersList');
},
store: new Vuex.Store({
getters: {
getUsersList: state => state.userResponse
},
actions: {
getUsersList
}
})
}).$mount("#app");
Also, use commit to reach the mutations instead of dispatch. ie:
export const getUsersList = function ({commit}) {
let url = '/Apis/allUsers';
Vue.http.get(url).then((response) => {
commit('GET_USER_RES', response.data); // because GET_USER_RES is a mutation
...

Resources