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.
Related
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
I have built a test app using nestjs + Sequelize ORM + docker database (as of now local). As per documentation, I am using umzug library and AWS Lambda SAM template and triggering lambda handler. Below is the code for it. Connection Pooling is implemented to reuse existing sequelize connection. Below is the lambdaEntry.ts file where I trigger umzug.up() function. It is triggering but not migrating files.
When done from command prompt node migrate up it works correctly. I am testing using sam invoke command to test it.
require('ts-node/register');
import { Server } from 'http';
import { NestFactory } from '#nestjs/core';
import { Context } from 'aws-lambda';
import * as serverlessExpress from 'aws-serverless-express';
import * as express from 'express';
import { ExpressAdapter } from '#nestjs/platform-express';
import { eventContext } from 'aws-serverless-express/middleware';
import { AppModule } from './app.module';
import sharedBootstrap from './sharedBootstrap';
const { Sequelize } = require('sequelize');
const { Umzug, SequelizeStorage } = require('umzug');
import configuration from '.././config/config';
const fs = require('fs');
let lambdaProxy: Server;
let sequelize = null;
async function bootstrap() {
const expressServer = express();
const nestApp = await NestFactory.create(
AppModule,
new ExpressAdapter(expressServer),
);
nestApp.use(eventContext());
sharedBootstrap(nestApp);
await nestApp.init();
return serverlessExpress.createServer(expressServer);
}
export const handler = (event: any, context: Context) => {
if (!lambdaProxy) {
bootstrap().then((server) => {
lambdaProxy = server;
serverlessExpress.proxy(lambdaProxy, event, context);
(async () => {
if (!sequelize) {
console.log('New connection::');
sequelize = await loadSequelize();
} else {
sequelize.connectionManager.initPools();
if (sequelize.connectionManager.hasOwnProperty('getConnection')) {
delete sequelize.connectionManager.getConnection;
}
}
try {
console.log('MIGRATOR::');
const umzug = new Umzug({
migrations: { glob: 'src/migrations/*.ts' },
context: sequelize.getQueryInterface(),
storage: new SequelizeStorage({ sequelize }),
logger: console,
});
await umzug
.pending()
.then((migrations: any) => {
console.log('pending ? : ', JSON.stringify(migrations));
//test for file exists.
for (const migration of migrations) {
try {
if (fs.existsSync(migration.path)) {
console.log('file exists');
}
} catch (err) {
console.log('file does not exists');
console.error(err);
}
}
async () => {
//BELOW FUNCTION IS TRIGGERING BUT NOT GETTING MIGRATION LOADED.
await umzug.up();
};
})
.catch((e: any) => console.log('error2 ? ', e));
} finally {
await sequelize.connectionManager.close();
}
})();
});
} else {
serverlessExpress.proxy(lambdaProxy, event, context);
}
};
async function loadSequelize() {
const sequelize = new Sequelize(
configuration.database,
configuration.username,
configuration.password,
{
dialect: 'mysql',
host: configuration.host,
port: Number(configuration.port),
pool: {
max: 2,
min: 0,
idle: 0,
acquire: 3000,
evict: 600,
},
},
);
await sequelize.authenticate();
return sequelize;
}
I am able to solve the issue after lot of tries. I seperated out the sequelize connection code and called it from app side and triggered from lambdaentry
lambdaEntry.js file.
async function bootstrap(uuid = null) {
console.log('Calling bootstrap');
const expressServer = express();
const nestApp = await NestFactory.create(
AppModule,
new ExpressAdapter(expressServer),
);
nestApp.use(eventContext());
sharedBootstrap(nestApp);
await nestApp.init();
try {
// Write a function in Service (ex: purhaslistservice) and trigger the service with umzug up from here.
const migrateResult1 = await nestApp.get(PurchaseListService).migrate('down');
console.log(migrateResult1);
const migrateResult2 = await nestApp.get(PurchaseListService).migrate('up');
console.log(migrateResult2);
} catch (err) {
throw err;
}
return serverlessExpress.createServer(expressServer);
}
export const handler = (event: any, context: Context) => {
if (!lambdaProxy) {
bootstrap(uuid).then((server) => {
lambdaProxy = server;
serverlessExpress.proxy(lambdaProxy, event, context);
});
} else {
serverlessExpress.proxy(lambdaProxy, event, context);
}
};
/code/src/purchaselist/purchaselist.service.ts
async migrate(id: string): Promise<any> {
console.log('migrate script triggered', id);
const sequelize = PurchaseListItem.sequelize;
const umzug = new Umzug({
migrations: { glob: 'src/migrations/*.{ts,js}' },
context: sequelize.getQueryInterface(),
storage: new SequelizeStorage({ sequelize }),
logger: console,
});
let consoleDisplay = 'Umzug LOGS:::<br/>';
switch (id) {
default:
case 'up':
await umzug.up().then(function (migrations) {
console.log('Umzug Migrations UP::<br/>', migrations);
consoleDisplay +=
'Umzug Migrations UP::<br/>' + JSON.stringify(migrations);
});
break;
}
return consoleDisplay;
}
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!
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())
I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
if an error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillMount of the parent component and the error will be handled in the Promise.catch of the callApi method that happens in the middleware.
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
This is because it is happening with in the same tick of the event loop. If I introduce some asynchronicity in the Autcomplete componentWIllMount function then the error is not handled in the Promise catch handler of the middleware
class Autocomplete extends Component{
componentWillMount() {
setTimeout(() => {
this.props.dispatch(actions.init({ props: this.exportProps() }));
});
}
Should I have the callApi function execute on a separate event loop tick?