Can we use dispatch inside action creators and what purpose do they serve inside action creators ?
Here is a sample modified code from codebase .
export default function xyz(data) {
const url = ;
return function (dispatch) {
dispatch(
a()
);
callApi(url, REQUESTS.POST, HEADERS, data).then((response) =>{
dispatch(
b(data)
);
}).catch((error) => {
dispatch(
c(error.toString())
);
});
};
}
// this returns a type (an object)
export function a() {
return {
type: xyzzzz
};
}
Similarly we have b and c returning either type or say objects .
Yes, you can dispatch multiple actions in an action.
I usually put a dispatch on an asychnronous action like this
function action() => {
return async (dispatch) => {
let payload;
dispatch('before-request');
try {
await someAsyncProcess();
payload = { status: 'success' };
} catch (err) {
payload = { status: 'failure' };
}
dispatch('after-request', payload);
};
}
Related
I am new to using sinon, so sorry if my question is weird, I looked everywhere but can't find a way to do it.
I have app with express router. I want to write uint test for one of the routes. That route have an inner function that is 'heavy', meaning that it is async with promise, and in reality calls an external api. I want to stub that inner function in the test so that it will not use the api, and will return my own data instead of the original method.
This is the code so far:
routes/setOrder.js:
// the inner function I want to stub
var verifyPayment = function(saleId) {
return new Promise((resolve, reject) => {
logger.info(`verifyPayment: ${saleId}`);
externalAPICall.get( // <==this is the 'heavey part!!
saleId,
function (error, sale) {
if(error) {
return reject(`Error querying sale(${saleId}): ${error}`);
}
resolve(sale);
});
});
}
router.get('/paymentId/:paymentId', setOrderWithGet);
const setOrderWithGet =async function(req, res, next) {
const { paymentId } = req.params;
verifyPayment(paymentId)
.then(async sale => {
try {
console.log(`sale:${sale}`);
res.send(JSON.stringify({"status": "ok!" }));
} catch (err) {
logger.warn(err)
res.send(JSON.stringify({"status": "fail.."}));
}
})
.catch(reason => {
logger.warn(`[] Payment(${paymentId}) is not valid ${reason}`);
res.send(JSON.stringify({"status": "fail.."}));
});
}
module.exports = router;
module.exports.setOrderWithGet = setOrderWithGet;
module.exports.verifyPayment = verifyPayment;
setOrderTest.js:
const setOrderStub = require('../routes/setOrder');
describe("POST /setOrder", () => {
beforeEach(() => {
sinon.stub(setOrderStub, 'verifyPayment').resolves({....});
});
afterEach(() => {
sinon.restore();
});
describe("test1", () => {
it("setOrder first attempt", () => {
let req ={params : {'paymentId' : 'mypamentid1'}};
setOrderStub.setOrderWithGet(req,{});
});
});
});
This line:
sinon.stub(setOrderStub, 'verifyPayment').resolves({....});
...stubs the verifyPayment function on the module exports of the setOrder module.
Right now setOrderWithGet is calling the verifyPayment function directly, so it is unaffected by any changes to the module exports.
Change setOrderWithGet to call verifyPayment using the module exports:
const setOrderWithGet = async function(req, res, next) {
// ...
module.exports.verifyPayment(paymentId) // <= call the module export for verifyPayment
// ...
}
...and your stub will get called.
I'm trying to create a simple middleware to handle socket events.
const join = (channel) => (dispatch) => {
dispatch({
type: 'ACTION-1',
socketChannel: {...},
events: [...],
});
};
I dispatch this action that triggers it. And now when the dispatch method was called in my middleware with type 'ACTION-2' and received socketData as a payload, I see in my console what 'ACTION-1' was triggered twice and in the last time it is came with my socketData payload.
I wonder why 'ACTION-1' was registered instead 'ACTION-2' and how I can fix it? I would appreciate your help.
import { socket } from 'services/socket';
const socketMiddleware = ({ dispatch }) => next => (action) => {
const {
channel,
events, // an array of events for the channel
...rest
} = action;
if (typeof action === 'function' || !channel) {
return next(action);
}
const {
type,
name,
} = channel;
const channelInstance = socket.instance[type](name);
events.forEach((event) => {
const handleEvent = (socketData) => {
dispatch({ type: 'ACTION-2', socketData, ...rest });
};
channelInstance.listen(event.name, handleEvent);
});
return next(action);
};
export {
socketMiddleware
};
looks like you are not pathing the channel in your initial dispatch and you are failing your middleware finishes inside this if:
if (typeof action === 'function' || !channel) {
return next(action);
}
in order to fix this you should add channel in your dispatch:
const join = (channel) => (dispatch) => {
dispatch({
type: 'ACTION-1',
socketChannel: {...},
events: [...],
channel: { type: '...', name: '...' }
});
};
when componentDidMount i dispatch an action to send a request, this is the action code below. receiveUserInfo just return data to reducer
export function requestUserInfo() {
return dispatch => {
dispatch(getUserInfo())
return axios.post('/user/getScores', qs.stringify({
token: token,
uid: uid
}))
.then(res => {
if (res.data.status !== 200) {
message.error(res.data.message)
return state
}
// return the data from server
return { ...res.data.attachment}
})
.then(data => {
// receiveUserInfo just return action and data
dispatch(receiveUserInfo(data))
})
}
}
this is the reducer code.
const token = localStorage.getItem('token')
const uid = localStorage.getItem('uid')
export const requestUserInfo = (state = {}, action) => {
switch (action.type) {
case 'HEADER_GET_USERINFO':
case 'HEADER_RECEIVE_USERINFO':
return Object.assign({}, state, action.data)
break
default:
return state
}
}
function receiveUserInfo(data) {
return {
type: 'HEADER_RECEIVE_USERINFO',
data
}
}
I created a rootSaga in sagas.js as
function* fetchStuff(action) {
try {
yield put({type: 'INCREMENT'})
yield call(delay, 1000)
yield put({type: 'DECREMENT'})
const highlights = yield call(API.getStuff, action.data.myObject);
} catch (e) {
yield put({type: 'FETCH_STUFF_FAILED', message: e});
}
}
export default function* rootSaga() {
yield takeEvery('INIT_LOAD', fetchStuff);
}
I am calling the INIT_LOAD after thirdParty.method:
class myClass extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.load();
}
load = () => {
this.init = () => {
this.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObject: this.myObject
}
};
store.dispatch(action);
});
};
this.init();
};
render() {
return (
<div id="render-here" />
);
}
Passing the this.myObject in the action that is dispatched does not trigger the saga. If I change the action payload to a string, like the following, the saga is triggered.
const action = {
type: 'INIT_LOAD',
payload: {
myObject: 'this.myObject'
}
};
Why am I unable to pass this.myObject but a string is ok?
UPDATE: It is not a saga issue. I replicated the same issue with just plain redux. The rootReducer as
export default function rootReducer(state = initialState, action) {
switch (action.type) {
case 'INIT_LOAD':
return Object.assign({}, state, { myObject: action.payload.myObject });
default:
return state;
}
}
As I mentioned in the comment below, assigning it to an object Obj does not change the issue
let Obj = {};
...
load = () => {
this.init = () => {
Obj.myObject = thirdParty.method(event => {
const action = {
type: 'INIT_LOAD',
payload: {
myObj: Obj
}
};
store.dispatch(action);
});
};
this.init();
};
UPDATE2
I cleaned the code up & simply dispatched an action in the component that triggers the saga. Inside the saga is where I do the init(). I ran into another issue where the object that I was trying to save in the redux store has active socket sessions (which were given me cross-domain issues). Although I didn't solve my original problem, not storing a socket object made my problem go away.
I'm trying to test a 'redux observable epic' but the test fail because not all actions are in store.getActions() the strange is the store.dispatch function runs.
Epic and actions
export const VERIFY_SESION = 'auth/VERIFY_SESION';
export const SET_POLICIES_ACCEPTED = 'auth/SET_POLICIES_ACCEPTED';
export const AUTHENTICATE = 'auth/AUTHENTICATE';
export function setPoliciesAccepted(wereAccepted: boolean) {
return {
wereAccepted,
type: SET_POLICIES_ACCEPTED,
};
}
export function verifySesion() {
return {
type: VERIFY_SESION,
};
}
export function authenticate(token) {
return {
token,
type: AUTHENTICATE,
};
}
export function verifySesionEpic(action$, store) {
return action$
.ofType(VERIFY_SESION)
.switchMap(async () => {
try {
store.dispatch(setBlockLoading(true));
const token = await AsyncStorage.getItem('token');
if (token !== null) {
store.dispatch(setBlockLoading(false));
return authenticate(token);
}
const policiesWereAccepted = await AsyncStorage.getItem('policiesWereAccepted');
store.dispatch(setBlockLoading(false));
return setPoliciesAccepted(policiesWereAccepted);
} catch (error) {
return setMessage(error.message);
}
});
}
test
describe('actions/auth', () => {
let store;
const asyncStorageGetStub = stub(AsyncStorage, 'getItem');
beforeEach(() => {
store = mockStore();
});
afterEach(() => {
asyncStorageGetStub.restore();
});
it('Should call authenticate if token', () => {
const token = 'mitoken';
asyncStorageGetStub.withArgs('token').returns(Promise.resolve(token));
store.dispatch(verifySesion());
expect(store.getActions()).toContain({ type: AUTHENTICATE, token });
});
});
Test result
1) "actions/auth Should call epic for verifySesion:
Error: Expected [ { type: 'auth/VERIFY_SESION' } ] to include { token: 'mitoken', type: 'auth/AUTHENTICATE' }"
Note
im sure that the conditional token !== null pass
I was to add a timeout before getAction because the 'AUTHENTICATE' actions is added after.
it('Should call authenticate if token', (done) => {
const token = 'mitoken';
asyncStorageGetStub.withArgs('token').returns(Promise.resolve(token));
store.dispatch(verifySesion());
setTimeout(() => {
expect(store.getActions()).toContain({ type: AUTHENTICATE, token });
done();
}, 1000);
});