I have some component with a code like this:
const startLogin = (code) => {
dispatch(login({ code }));
const publicKeyFromLocalST = window.localStorage.getItem('push_public_key');
setPublicKey(publicKeyFromLocalST);
// etc
When I dispatch the saga login it will store some data in localStorage.
I need to execute the 3rd line (setPublicKey) after that data be actually indeed in localStorage.
How can "await" for dispatch(login({ code })); to be completed before setPublicKey?
Two options:
Execute the setPublicKey function inside the worker saga, you can control the workflow in the worker saga easily with yield.
function* login(action) {
const response = yield call(apiCall);
if (response.error) {
yield put({ type: actionType.LOGIN_FAIL });
} else {
yield put({ type: actionType.LOGIN_SUCCESS, data: response.data });
const publicKeyFromLocalST = window.localStorage.getItem('push_public_key');
setPublicKey(publicKeyFromLocalST);
}
}
Promisify the dispatch(login({code})), you should create a helper function like this:
const loginAsyncCreator = (dispatch) => (payload) => {
return new Promise((resolve, reject) => dispatch(loginCreator(payload, { resolve, reject })));
};
You need to pass the resolve/reject to worker saga via action.meta, then you can decide when to resolve or reject the promise. Then, you can use async/await in your event handler. See below example:
import { call, put, takeLatest } from 'redux-saga/effects';
import { createStoreWithSaga } from '../../utils';
const actionType = {
LOGIN: 'LOGIN',
LOGIN_FAIL: 'LOGIN_FAIL',
LOGIN_SUCCESS: 'LOGIN_SUCCESS',
};
function apiCall() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ error: null, data: 'login success' });
}, 2000);
});
}
function* login(action) {
console.log('action: ', action);
const {
meta: { resolve, reject },
} = action;
const response = yield call(apiCall);
console.log('response: ', response);
if (response.error) {
yield put({ type: actionType.LOGIN_FAIL });
yield call(reject, response.error);
} else {
yield put({ type: actionType.LOGIN_SUCCESS, data: response.data });
yield call(resolve, response.data);
}
}
function* watchLogin() {
yield takeLatest(actionType.LOGIN, login);
}
const store = createStoreWithSaga(watchLogin);
function loginCreator(payload, meta) {
return {
type: actionType.LOGIN,
payload,
meta,
};
}
const loginAsyncCreator = (dispatch) => (payload) => {
return new Promise((resolve, reject) => dispatch(loginCreator(payload, { resolve, reject })));
};
const loginAsync = loginAsyncCreator(store.dispatch);
async function startLogin() {
await loginAsync({ code: '1' });
console.log('setPublicKey');
}
startLogin();
The logs:
action: {
type: 'LOGIN',
payload: { code: '1' },
meta: { resolve: [Function (anonymous)], reject: [Function (anonymous)] }
}
response: { error: null, data: 'login success' }
setPublicKey
Related
Basically i wanted to integrate the mqtt in my react app. I want to connect to the mqtt when user login and disconnect when user logs out.
Firstly i created actions,reducers to dispatch the connection state and get client payload. It's connecting but the mqtt servers seems to be reconnecting in a loop with few secs. I want it to connect once and disconnect user logs out automatically. I globally initiated the mqtt client in the mqqtActions.js.
import { CONNECT_MQTT_FAILURE, CONNECT_MQTT_REQUEST, CONNECT_MQTT_SUCCESS, DISCONNECT_MQTT_FAILURE, DISCONNECT_MQTT_REQUEST, DISCONNECT_MQTT_SUCCESS } from "../constants/mqttConstants"
import mqtt from 'mqtt'
const mqttHost = "host ip address here";
const mqttPort = 8083;
const mqttUrl = `ws://${mqttHost}:${mqttPort}/mqtt`
const mqttOptions = {
keepalive: 30,
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
reconnectPeriod: 1000,
connectTimeout: 30 * 1000,
clientId: "bharath",
will: {
topic: 'WillMsg',
payload: 'Connection Closed abnormally..!',
qos: 0,
retain: false
},
rejectUnauthorized: false
};
const mqttClient = mqtt.connect(mqttUrl, mqttOptions)
export const mqttConnect = () => async (dispatch) => {
try {
dispatch({
type: CONNECT_MQTT_REQUEST
})
dispatch({
type: CONNECT_MQTT_SUCCESS,
payload: mqttClient
})
//mqttClient.connect()
localStorage.setItem('mqttClient', JSON.stringify(mqttClient))
} catch (error) {
dispatch({
type: CONNECT_MQTT_FAILURE,
payload: error.response && error.response.data.message ? error.response.data.message : error.message
})
}
}
export const mqttDisconnect = () => (dispatch) => {
try {
dispatch({ type: DISCONNECT_MQTT_REQUEST })
mqttClient.end()
localStorage.removeItem('mqttClient')
dispatch({
type: DISCONNECT_MQTT_SUCCESS,
})
} catch (error) {
dispatch({
type: DISCONNECT_MQTT_FAILURE,
payload: error.response && error.response.data.message ? error.response.data.message : error.message
})
}
}
I set up my reducer file like this:
import { CONNECT_MQTT_FAILURE, CONNECT_MQTT_REQUEST, CONNECT_MQTT_SUCCESS, DISCONNECT_MQTT_FAILURE, DISCONNECT_MQTT_REQUEST, DISCONNECT_MQTT_SUCCESS } from "../constants/mqttConstants"
export const connectMqttReducer = (state = {}, action) => {
switch (action.type) {
case CONNECT_MQTT_REQUEST:
return { status: 'connecting' }
case CONNECT_MQTT_SUCCESS:
return { status: 'connected', client: action.payload }
case CONNECT_MQTT_FAILURE:
return { status: 'connect', error: action.payload }
default:
return state
}
}
export const disconnectMqttReducer = (state = {}, action) => {
switch (action.type) {
case DISCONNECT_MQTT_REQUEST:
return { status: 'connected' }
case DISCONNECT_MQTT_SUCCESS:
return { status: 'connect' }
case DISCONNECT_MQTT_FAILURE:
return { status: 'connected', error: action.payload }
default:
return state
}
}
Doing this i'm able to connect but its timestamp when connectedAt is changing continously.And also mqtt.connect() is not function error is also showing. I commented it out. I want to connect it once and disconnect when user login and logout actions are triggered.
Please try the following:
1- Add the following to your environment file:
MQTT_HOST="host ip address here"
MQTT_PORT=8083
2- Add the following in your Constants.js file:
export const mqttUrl = `ws://${process.env.MQTT_HOST}:${process.env.MQTT_PORT}/mqtt`;
export const mqttOptions = {
keepalive: 30,
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
reconnectPeriod: 1000,
connectTimeout: 30 * 1000,
clientId: 'bharath',
will: {
topic: 'WillMsg',
payload: 'Connection Closed abnormally..!',
qos: 0,
retain: false,
},
rejectUnauthorized: false,
};
export const localStorageKeys = {
mqttClient: 'mqttClient',
};
3- Create a new file which will hold all mqtt functionality /src/clients/MqttClient.js:
import mqtt from 'mqtt';
import { localStorageKeys, mqttOptions, mqttUrl } from '#/js/constants/Helpers';
let instance = null;
class MqttClient {
constructor() {
if (!instance) {
instance = this;
}
return instance;
}
myMqtt;
connect() {
this.myMqtt = mqtt.connect(mqttUrl, mqttOptions);
return new Promise((resolve, reject) => {
this.myMqtt.on('connect', () => {
//add instance to local storage if it's not present (if you need it)
const localStorageMqtt = localStorage.getItem(localStorageKeys.mqttClient);
if (localStorageMqtt === null) {
localStorage.setItem(localStorageKeys.mqttClient, JSON.stringify(this.myMqtt));
}
resolve();
});
this.myMqtt.on('error', (error) => reject(error));
});
}
disconnect() {
return new Promise((resolve) => {
this.myMqtt.end(false, {}, () => {
this.myMqtt = null;
//if you added it to the localstorage (on connect)
localStorage.removeItem(localStorageKeys.mqttClient);
resolve();
});
});
}
subscribe(event) {
return new Promise((resolve, reject) => {
if (!this.myMqtt) return reject('No mqtt connection.');
return this.myMqtt.subscribe(event, (err) => {
// Optional callback that you can use to detect if there's an error
if (err) {
console.error(err);
return reject(err);
}
return resolve();
});
});
}
publish(event, data) {
return new Promise((resolve, reject) => {
if (!this.myMqtt) return reject('No mqtt connection.');
return this.myMqtt.publish(event, data, {}, (err) => {
// Optional callback that you can use to detect if there's an error
if (err) {
console.error(err);
return reject(err);
}
return resolve();
});
});
}
on(event, fun) {
// No promise is needed here, but we're expecting one in the middleware.
return new Promise((resolve, reject) => {
if (!this.myMqtt) return reject('No mqtt connection.');
this.myMqtt.on(event, fun);
resolve();
});
}
}
export default MqttClient;
4- Create a new mqtt middleware in your store directory /src/store/middleWares/MqttMiddleWare.js:
export const mqttMiddleWare =
(mqtt) =>
({ dispatch, getState }) =>
(next) =>
(action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
/*
* Mqtt middleware usage.
* promise: (mqtt) => mqtt.connect()
* type: 'mqtt' //always (mqtt)
* types: [REQUEST, SUCCESS, FAILURE]
*/
const { promise, type, types, ...rest } = action;
if (type !== 'mqtt' || !promise) {
// Move on! Not a mqtt request or a badly formed one.
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = types;
next({ ...rest, type: REQUEST });
return promise({ mqtt, dispatch, getState })
.then((result) => {
return next({ ...rest, result, type: SUCCESS });
})
.catch((error) => {
console.log(error);
return next({ ...rest, error, type: FAILURE });
});
};
5- Update your store config to accept mqtt client as an argument then pass it to the mqtt middleware as follows /src/store/configureStore.js:
const middlewares = [];
// log redux data in development mode only
if (process.env.NODE_ENV !== 'production') {
const { logger } = require('redux-logger');
middlewares.push(logger);
}
const configureStore = (mqttClient) => {
const store = createStore(
rootReducer,
/* preloadedState, */
composeWithDevTools(
applyMiddleware(thunkMiddleware, mqttMiddleWare(mqttClient), ...middlewares)
)
);
return store;
};
export default configureStore;
6- Instantiate your mqtt client in /src/index.jsx and pass it to your store:
const mqttClient = new MqttClient();
const store = configureStore(mqttClient);
7- Update your reducer as follows:
import { CONNECT_MQTT_FAILURE, CONNECT_MQTT_REQUEST, CONNECT_MQTT_SUCCESS, DISCONNECT_MQTT_FAILURE, DISCONNECT_MQTT_REQUEST, DISCONNECT_MQTT_SUCCESS } from "../constants/mqttConstants"
export const connectMqttReducer = (state = {}, action) => {
switch (action.type) {
case CONNECT_MQTT_REQUEST:
return { connectionStatus: 'connecting' }
case CONNECT_MQTT_SUCCESS:
return { connectionStatus: 'connected' }
case CONNECT_MQTT_FAILURE:
return { connectionStatus: 'connect failed', error: action.error }
default:
return state
}
}
export const disconnectMqttReducer = (state = {}, action) => {
switch (action.type) {
case DISCONNECT_MQTT_REQUEST:
return { disconnectionStatus: 'disconnecting' }
case DISCONNECT_MQTT_SUCCESS:
return { disconnectionStatus: 'disconnected' }
case DISCONNECT_MQTT_FAILURE:
return { disconnectionStatus: 'connect failed', error: action.error }
default:
return state
}
}
8- Update your actions as follows:
Connect action:
export const startMqttConnection = () => ({
type: 'mqtt',
types: [CONNECT_MQTT_REQUEST, CONNECT_MQTT_SUCCESS, CONNECT_MQTT_FAILURE],
promise: ({ mqtt }) => mqtt.connect(),
});
Disconnect action:
export const stopMqttConnection = () => ({
type: 'mqtt',
types: [DISCONNECT_MQTT_REQUEST, DISCONNECT_MQTT_SUCCESS, DISCONNECT_MQTT_FAILURE],
promise: ({ mqtt }) => mqtt.disconnect(),
});
9- dispatch the required action as follows:
useEffect(() => {
dispatch(startMqttConnection());
return () => {
if (connectionStatus === 'connected') {
dispatch(stopMqttConnection());
}
};
//eslint-disable-next-line
}, [dispatch]);
Reducer :
function listPeopleByName (state = {
getPeopleName:{}
}, action){
switch(action.type){
case C.LIST_PEOPLE_NAME:{
return {
...state
,getPeopleName :action.payload
}
}
default : {}
}
return state
}
Action:
function listPeopleByName(config) {
return function (dispatch) {
ApiService(config)
.then((resp) => {
dispatch({
type: C.LIST_PEOPLE_NAME,
payload: resp.data,
});
})
.catch((error) => {
dispatch({
type: C.LIST_PEOPLE_NAME,
payload: error,
});
});
};
}
ApiService is a function that make an axios request and returns a respones
Dispatching code :
listPeopleByNameFunction = () => {
const listPeopleByNameParam = {
id: someone,
},
let data = {
PeopleId: "snjenfner",
};
let listPeopleByNameCategory = getApiConfig(
"POST",
listPeopleByNameParam,
data
);
this.props.listPeopleByName(listPeopleByNameCategory);
};
const mapDispatchToProps = (dispatch) => ({
listPeopleByName: (config) => dispatch(listPeopleByName(config)),
});
Although I take the previous state (...state) and change the state with the payload i'm getting, it still shows the state is mutated. I would have used reduxtoolkit but this is a way old project that doesn't need to be migrated to reduxtoolkit.
I am trying to get myLocation(redux state) variable which is used in next dispatch GET_POSTS_REQUEST. But when i tried to put await to get fully return value, it shows error.
index.js
const testFunc = () => {
const { myLocation} = useSelector(state => state.user);
dispatch({
type: MY_LOCATION_REQUEST,
data: {
lat: position.coords.latitude,
long: position.coords.longitude,
},
});
dispatch({
type: GET_POSTS_REQUEST,
data: {
dong: myLocation.dong,
},
});
};
sagas/location.js
function getLocationAPI(locationInfo) {
return axios.post('/location', locationInfo ,{withCredentials: true});
}
function* getLocation(action) {
try {
const result = yield call(getLocationAPI, action.data);
yield put({
type: GET_LOCATION_SUCCESS,
data: result.data,
});
} catch (e) {
yield put({
type: GET_LOCATION_FAILURE,
error: e,
});
}
}
function* watchGetLocation() {
yield takeLatest(GET_LOCATION_REQUEST, getLocation);
}
export default function* locationSaga() {
yield all([
fork(watchGetLocation),
]);
}
I have to use myLocation for next dispatch action in index.js. But, when i tried to put async/await to my dispatch, it didn't work. Is there any solution for this?
put method kinda dispatches too, for example this part
yield put({
type: GET_LOCATION_SUCCESS,
data: result.data,
});
you can always see as
dispatch({
type: GET_LOCATION_SUCCESS,
data: result.data,
});
So you have to "catch" this with reducer, for example
function yourReducer(state = initialState, action) {
switch (action.type) {
case GET_LOCATION_SUCCESS:
return Object.assign({}, state, {
user: Object.assign({},
state.user,
{
myLocation: action.data
}
]
})
default:
return state
}
}
And this will update your state with data returned from api call
So similar to some previous posts referenced below, I'm trying to chain dispatch through Thunk, however my difficulty is on the return from Sequelize. I can see the MySQL query hit the DB and return data, however that return is not flowing through the action-creator to the subsequent .then
I presume it's the manner in which I'm trying to use Sequelize - I'm new to it.
Many thanks!
Code:
initDB.js
...
export function sequelizeQry(qryName: string) {
let query;
// For later query mapping
switch (qryName) {
case 'POSummary':
query = qry.spPOLedgerCardSummaryALL;
break;
default:
query = qry.spPOLedgerCardSummaryALL;
}
return new sequelize.Promise(
() => sequelize.query(query, { type: sequelize.QueryTypes.RAW })
.then((response) => {
console.log('Returning promise: ', response); //<-- This hits the console with data
return response;
})
);
}
database-actions.js
// #flow
import { fetchingQuery } from '../action-creators/database-creators';
const fetchQuery = (qryName: string) =>
(dispatch: *) => dispatch(fetchingQuery(qryName));
export default fetchQuery;
database-creators.js
// #flow
// Action Types
import { FETCH_QUERY } from '../constants/ActionTypes';
import { sequelizeQry } from '../utils/initDB';
/** Action-creators */
export function fetchingQuery(qryName: string) {
const actionType = FETCH_QUERY;
return (dispatch: *) => {
dispatch({ type: `${actionType}_PENDING` });
sequelizeQry(qryName) //<-- This gets called
.then((payload) => dispatch({ //<-- Don't seem to get here
type: `${actionType}_FULFILLED`,
payload
}))
.catch(err =>
// Dispatch the error action with error information
dispatch({
type: `${actionType}_REJECTED`,
error: err
})
);
};
}
Some other references I've checked:
Redux thunk: return promise from dispatched action
return promise from store after redux thunk dispatch
All credit goes to adrice727.
Here's the code change for future visitors:
...
return new sequelize.Promise(
() => sequelize.query(query, { type: sequelize.QueryTypes.RAW })
.then((response) => {
console.log('Returning promise: ', response); //<-- This hits the console with data
return response;
})
);
...
// BECOMES
return new sequelize.Promise(
(resolve) => sequelize.query(query, { type: sequelize.QueryTypes.RAW })
.then((response) => {
console.log('Returning promise: ', response);
return resolve(response);
})
);
action.js
export function getLoginStatus() {
return async(dispatch) => {
let token = await getOAuthToken();
let success = await verifyToken(token);
if (success == true) {
dispatch(loginStatus(success));
} else {
console.log("Success: False");
console.log("Token mismatch");
}
return success;
}
}
component.js
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
});
}
However, when I write component.js code with async/await like below I get this error:
Possible Unhandled Promise Rejection (id: 0): undefined is not a function (evaluating 'this.props.dispatch(splashAction.getLoginStatus())')
component.js
async componentDidMount() {
let success = await this.props.dispatch(splashAction.getLoginStatus());
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
}
How do I await a getLoginStatus() and then execute the rest of the statements?
Everything works quite well when using .then(). I doubt something is missing in my async/await implementation. trying to figure that out.
The Promise approach
export default function createUser(params) {
const request = axios.post('http://www...', params);
return (dispatch) => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
request.then(success => onSuccess, error => onError);
};
}
The async/await approach
export default function createUser(params) {
return async dispatch => {
function onSuccess(success) {
dispatch({ type: CREATE_USER, payload: success });
return success;
}
function onError(error) {
dispatch({ type: ERROR_GENERATED, error });
return error;
}
try {
const success = await axios.post('http://www...', params);
return onSuccess(success);
} catch (error) {
return onError(error);
}
}
}
Referenced from the Medium post explaining Redux with async/await: https://medium.com/#kkomaz/react-to-async-await-553c43f243e2
Remixing Aspen's answer.
import axios from 'axios'
import * as types from './types'
export function fetchUsers () {
return async dispatch => {
try {
const users = await axios
.get(`https://jsonplaceholder.typicode.com/users`)
.then(res => res.data)
dispatch({
type: types.FETCH_USERS,
payload: users,
})
} catch (err) {
dispatch({
type: types.UPDATE_ERRORS,
payload: [
{
code: 735,
message: err.message,
},
],
})
}
}
}
import * as types from '../actions/types'
const initialErrorsState = []
export default (state = initialErrorsState, { type, payload }) => {
switch (type) {
case types.UPDATE_ERRORS:
return payload.map(error => {
return {
code: error.code,
message: error.message,
}
})
default:
return state
}
}
This will allow you to specify an array of errors unique to an action.
Another remix for async await redux/thunk. I just find this a bit more maintainable and readable when coding a Thunk (a function that wraps an expression to delay its evaluation ~ redux-thunk )
actions.js
import axios from 'axios'
export const FETCHING_DATA = 'FETCHING_DATA'
export const SET_SOME_DATA = 'SET_SOME_DATA'
export const myAction = url => {
return dispatch => {
dispatch({
type: FETCHING_DATA,
fetching: true
})
getSomeAsyncData(dispatch, url)
}
}
async function getSomeAsyncData(dispatch, url) {
try {
const data = await axios.get(url).then(res => res.data)
dispatch({
type: SET_SOME_DATA,
data: data
})
} catch (err) {
dispatch({
type: SET_SOME_DATA,
data: null
})
}
dispatch({
type: FETCHING_DATA,
fetching: false
})
}
reducers.js
import { FETCHING_DATA, SET_SOME_DATA } from './actions'
export const fetching = (state = null, action) => {
switch (action.type) {
case FETCHING_DATA:
return action.fetching
default:
return state
}
}
export const data = (state = null, action) => {
switch (action.type) {
case SET_SOME_DATA:
return action.data
default:
return state
}
}
Possible Unhandled Promise Rejection
Seems like you're missing the .catch(error => {}); on your promise. Try this:
componentDidMount() {
this.props.dispatch(splashAction.getLoginStatus())
.then((success) => {
if (success == true) {
Actions.counter()
} else {
console.log("Login not successfull");
}
})
.catch(err => {
console.error(err.getMessage());
}) ;
}
use dispatch(this.props.splashAction.getLoginStatus()) instead this.props.dispatch(splashAction.getLoginStatus())