I'm using react redux to create an action creator in my app. The point is that when I use async await syntax, it auto returns a promise (without the "return" keyword). However, when I use old-style promise like then(), i have to explicitly type the "return" keyword - otherwise it will return undefined. Why does this happen?
app.js (createStore):
app.get('*', (req, res) => {
const store = createStore(reducers, applyMiddleware(reduxThunk));
const promise = matchRoutes(RouteApp, req.path).map(({ route }) => {
return route.loadData ? route.loadData(store) : null;
});
console.log(promise);
Promise.all(promise).then(() => {
res.send(renderApp(req, store));
});
});
route.js:
export default [
{
loadData,
path: '/',
component: Landing,
exact: true,
},
];
landing.js
function loadData(store) {
return store.dispatch(fetchUser());
}
export { loadData };
When I use async await:
action.js
export const fetchUser = () => async (dispatch) => {
const res = await axios.get('https://react-ssr-api.herokuapp.com/users');
dispatch({
type: INFO_USER,
payload: res.data,
});
};
When I use promise then:
// It doesn't work
export const fetchUser = () => (dispatch) => {
axios.get('https://react-ssr-api.herokuapp.com/users').then((res) => {
dispatch({
type: INFO_USER,
payload: res.data,
});
});
};
"return" keyword
// now it works
export const fetchUser = () => (dispatch) => {
return axios.get('https://react-ssr-api.herokuapp.com/users').then((res) => {
dispatch({
type: INFO_USER,
payload: res.data,
});
});
};
async function always returns a promise, that's its purpose. In case there's no return value, it returns a promise of undefined.
As the reference states,
Return value
A Promise which will be resolved with the value returned by the async
function, or rejected with an uncaught exception thrown from within
the async function.
This async function
export const fetchUser = () => async (dispatch) => {
const res = await axios.get('https://react-ssr-api.herokuapp.com/users');
dispatch({
type: INFO_USER,
payload: res.data,
});
};
is syntactic sugar for this function:
export const fetchUser = () => (dispatch) => {
return axios.get('https://react-ssr-api.herokuapp.com/users').then((res) => {
dispatch({
type: INFO_USER,
payload: res.data,
});
});
};
Related
I am writing a test for async action creator but encountered a problem where it states "Expected mock function to have been called" with:
[{"type": "GET_LOGIN_SUCCESS", "value": true}]
But it was not called.
I am not sure where exactly the problem is. If anyone could help that will be greatly appreciated.
Here's my actions.js
import { GET_LOGIN_SUCCESS } from './constants'
export const getLoginInfo = () => {
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get('/api/isLogin.json')
.then((res) => {
dispatch({
type: GET_LOGIN_SUCCESS,
value: res.data.data.login
})
console.log('finishing dispatch')
})
}
}
actions.test.js
import { getLoginInfo } from './actions'
import { GET_LOGIN_SUCCESS } from './constants'
describe('async actions', () => {
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', () => {
const axiosInstance = {
get: jest.fn(() => Promise.resolve({ data: { data: {login : true }}}))
}
const dispatch = jest.fn()
getLoginInfo()(dispatch, null, axiosInstance)
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
})
The problem is that jest can't know that there are async task involved. So in your case you create a mock that returns a promise, and dispatch is called when the promise is resolved. As JavaScript is single threaded, it first evaluate the code in the test and all async tasks are done afterwards. So you need to make jest aware of the promise by using async/await:
describe('async actions', () => {
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', async() => {
const p = Promise.resolve({ data: { data: {login : true }}}))
const axiosInstance = {
get: jest.fn(() => p
}
const dispatch = jest.fn()
getLoginInfo()(dispatch, null, axiosInstance)
await p // even it sounds not very logically you need to wait here
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
As #brian-lives-outdoors points out, getLoginInfo returns the promise as well so you could also just wait for the result of the call:
it('dispatches GET_LOGIN_SUCCESS when getting login finishes', async() => {
const axiosInstance = {
get: jest.fn(() => Promise.resolve({ data: { data: {login : true }}}))
}
const dispatch = jest.fn()
await getLoginInfo()(dispatch, null, axiosInstance)
expect(dispatch).toHaveBeenCalledWith({
type: GET_LOGIN_SUCCESS,
value: true
})
})
There is a epic article that describes the whole topic
I am using redux-thunk and want like to dispatch an action and once that is finished make an api call with part of that updated store.
store.js
const middleware = composeEnhancers(applyMiddleware(promise(), thunk, logger()))
const localStore = loadStore()
const store = createStore(reducer, localStore, middleware)
graphActions.js:
First add an Element:
export function addElement(element) {
return dispatch => {
dispatch({
type: ADD_ELEMENT,
payload: element
})
}
}
Then make api call via different action creator:
export function saveElements() {
return (dispatch, getState) => {
let graphId = getState().elements.id
let elements = getState().elements.elements
axios.put(Config.config.url + '/graph/' + graphId, {
'data': JSON.stringify({elements: elements}),
}).then(() => {
dispatch({type: SHOW_SUCCESS_SNACKBAR})
}).catch((err) => {
dispatch({type: SHOW_ERROR_SNACKBAR})
dispatch({type: UPDATE_ELEMENTS_REJECTED, payload: err})
})
}
}
I need to make sure, that addElement() is finished before saveElements(), so that saveElements() accesses the updated store.
I tried the following:
export function addElement(element) {
const promise = (dispatch) => new Promise((resolve) => {
dispatch({
type: ADD_ELEMENT,
payload: element
})
resolve()
})
return dispatch => {
promise(dispatch).then(() => {
saveElements()
})
}
}
ADD_ELEMENT is dispatched, but the actions within saveElements() are not dispatched, no api call is made.
I was missing to dispatch saveElements() and returning dispatch(saveElements()).
export function addElement(element) {
const promise = (dispatch) => new Promise((resolve) => {
dispatch({
type: ADD_ELEMENT,
payload: element
})
resolve()
})
return (dispatch) => {
return addElements(dispatch).then(() => {
return dispatch(saveElements())
})
}
UPDATE:
Noticed I can simply do:
export function addElement(element)
return (dispatch) => {
dispatch({
type: ADD_ELEMENT,
payload: element
})
dispatch(saveElements())
})
}
In the following code the 'callback' parameter is a function in my component that called the action creator, however I cannot seem to work out where to evoke this callback in my action creator. I'm using ReduxThunk for the async functionality.
My main objective is to fire the callback after the successful request is made.
import axios from 'axios';
export const FETCH_USERS = 'FETCH_USERS';
const API_URL_TOP30 = 'https://fcctop100.herokuapp.com/api/fccusers/top/recent';
const API_URL_ALLTIME = 'https://fcctop100.herokuapp.com/api/fccusers/top/alltime';
export function fetchUsers(list, callback) {
const apicall = (list) ? API_URL_ALLTIME : API_URL_TOP30
const data = ''
const request = axios.get(apicall)
return (dispatch) => {
request.then(({data}) => {
dispatch({ type: FETCH_USERS, payload: data })
})
}
}
invoke the callback in then method
export function fetchUsers(list, callback) {
const apicall = (list) ? API_URL_ALLTIME : API_URL_TOP30
const data = ''
const request = axios.get(apicall)
return (dispatch) => {
request.then(({ data }) => {
callback(data);
dispatch({ type: FETCH_USERS, payload: data })
})
}
}
Instead of passing a callback as a second parameter to your function, you could just return your promise from that function.
export function fetchUsers(list) {
const apicall = (list) ? API_URL_ALLTIME : API_URL_TOP30
const data = ''
const request = axios.get(apicall)
return (dispatch) => {
return request.then(({data}) => {
dispatch({ type: FETCH_USERS, payload: data })
})
}
}
This makes it possible to use .then() from your component, since your function returns a promise.
*** Your component ***
fetchUsers(list).then(() => {
console.log('success')
})
I was testing redux actions using Jest , When i try to test the default action, it throws an Error
Expected value to equal:
{"payload": {"male": "mancha"}, "type": "actions/change_gender"}
Received:
[Function anonymous]
It seems it sends the function, instead of values.
test change_gender.js
import changeGender, { CHANGE_GENDER } from '../change_gender';
const payload = {
type: CHANGE_GENDER,
payload: {
male: 'mancha'
}
};
describe('actions', () => {
it('should Change the ', () => {
const expectedAction = {
type: payload.type,
payload: payload.payload
};
expect(changeGender('male', 'mancha')).toEqual(expectedAction)
});
});
Action change_gender.js
import toggleToolTip from './toggle_tooltip'; // eslint-disable-line
export const CHANGE_GENDER = 'actions/change_gender';
export default(radioType, type) => (dispatch) => {
dispatch({
type: CHANGE_GENDER,
payload: {
[radioType]: type
}
});
};
You should return the dispatch at change_gender.js:
change_gender.js:
import toggleToolTip from './toggle_tooltip'; // eslint-disable-line
export const CHANGE_GENDER = 'actions/change_gender';
export default(radioType, type) => (dispatch) => {
return dispatch({
type: CHANGE_GENDER,
payload: {
[radioType]: type
}
});
};
As Chen-tai mentioned, returning from the dispatch would help here for testing purposes.
The reason you see [Function] being returned is that your action is a function returning a function.
(radioType, type) => (dispatch) => { ... }
The first set of params, followed by the fat arrow is an anonymous function. That then returns another anonymous function that takes a dispatch function as its arguments. So, if we call the action twice, providing a mock dispatch function, we'll get back the expected action!
const action = (radioType, type) => (dispatch) => {
return dispatch({
type: "CHANGE_GENDER",
payload: {
[radioType]: type
}
});
};
console.log(
action('male', 'mancha')((action) => action)
)
We can then write out test:
Action change_gender.js
import toggleToolTip from './toggle_tooltip'; // eslint-disable-line
export const CHANGE_GENDER = 'actions/change_gender';
export default(radioType, type) => (dispatch) => {
return dispatch({
type: CHANGE_GENDER,
payload: {
[radioType]: type
}
});
};
test change_gender.js:
import changeGender, { CHANGE_GENDER } from '../change_gender';
const payload = {
type: CHANGE_GENDER,
payload: {
male: 'mancha'
}
};
describe('actions', () => {
it('should Change the ', () => {
const expectedAction = {
type: payload.type,
payload: payload.payload
};
expect(changeGender('male', 'mancha')((payload) => payload).toEqual(expectedAction)
});
});
I have a problem with my epic, please help me find out where I went wrong. Thank you!
The following code leads me to the error:
const loginEpic = (action$) =>
action$
.ofType('LOGIN')
.switchMap(() => {
return Observable.fromPromise(loginService())
.map((result) => {
return Observable.of({
payload: result,
type: types.loginCompleted,
});
})
.catch((error) => {
return Observable.of({
payload: error,
type: types.loginFailed,
});
});
});
And here is my configureStore file:
const epicMiddleware = createEpicMiddleware(rootEpic);
// Ref: https://redux-observable.js.org/docs/recipes/HotModuleReplacement.html
if (module.hot) {
module.hot.accept('./epic', () => {
const nextEpic = require('./epic');
epicMiddleware.replaceEpic(nextEpic);
});
}
const configureStore = (): Store<any> => {
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(epicMiddleware)));
if (module.hot) {
module.hot.accept('./reducer', () => {
const nextReducer = require('./reducer').default;
store.replaceReducer(nextReducer);
});
return store;
}
};
I believe you must drop the Observable.of(...) from within the map() (EDIT - thanks to paulpdaniels: but not the catch()) method, because in this way you are returning an observable of observables - see the simplified code below:
Observable.fromPromise(...)
.map(result => Observable.of(...)) // maps an object to an observable
The entire code should be:
const loginEpic = (action$) =>
action$
.ofType('LOGIN')
.switchMap(() => {
return Observable.fromPromise(loginService())
.map((result) => {
return {
payload: result,
type: types.loginCompleted,
};
})
.catch((error) => {
return Observable.of({
payload: error,
type: types.loginFailed,
});
});
});