I have a function in redux store.
Func1(){
Dispatch(some action)
Dispatch(some action1)
result = Dispatch(some async action)\\promise retured
result.then(()=>
Dispatch(some other action)
)
}
My test case
Test(()=>{
Expectedactions= all 3 actions.
Axios.post.mockimplimetation(()=>
Promise.resolve(data))
calling Func1() from wrapper.
Expect(store.getActions()).toequal(expectedactions)
})
Test result shows only 2 actions.
Async action is not captured.
Please help me how to test this behaviour or what approach I shoud take.
Edited.
TestConrainer.js
const mapstatetoprops= (state)=>{
return {}
}
const mergeprops =(stateprops,dispatchprops,ownprops)=>{
const {dispatch}= dispatchprops
const f1=()=>{
dispatch(configActions.pause("Pause"))
let result = dispatch(DataActions.save(stateProps.data)
dispatch(configActions.setClick(false));
result.then((result) => {
dispatch(configActions.show(false));
dispatch(DataActions.StatusValue(""))
})
return Object.assign({},stateprops,ownprops,{
f1:f1
})
}
export default connect(mapstatetoprops,null,mergeprops)(Test)
DataActions.js
export const save = (data) => {
return (dispatch,getState) =>{
return axios.post(url, payload))
.then(data => {
return data;
},error => {
return error
}).catch(err){
return err
}
My test case
Test(()=>{
Expectedactions= all 3 actions.
Axios.post.mockimplimetation(()=>
Promise.resolve(data))
calling f1() from wrapper like
Wrapper.shallow(<TestContainer store=store/>)
Wrapper.find('Test').props().f1()
Expect(store.getActions()).toequal(expectedactions)
})
Test result shows only 2 actions.
Async action is not captured.
Please help me how to test this behaviour or what approach I shoud take.
Related
export const loginSuccess = createAsyncThunk(
"auth/loginSuccess",
async (user: User) => {
const res = await api
.post(
"/auth/loginSuccess",
{ user },
{
withCredentials: true,
}
)
.then((res: any) => {
setAxiosToken(res.data.token);
saveToken(res.data.token);
return { ...res.data.data, token: res.data.token };
});
return res;
}
);
There are 2 return statements at the end so I am confused about which return value the fulfilled reducer will get. The code is written by someone else that's why I want to understand it.
The second return statement is the one which will return from your function.
The first is actually returning from the then function of the promise that axios returns.
This is made a little bit confusing by using the same name for the res variable in the thunk function, and for the response variable that is passed on the the then function.
But what you will receive back is the object generated in this line of code:
{ ...res.data.data, token: res.data.token }
Where res.data.data is spread into a new object, and res.data.token is assigned to the token property of that object.
When I'm try to getUserProfile() I receive that typeError that dispatch is not a function
Unhandled Runtime Error
Error: Actions must be plain objects. Use custom middleware for async actions.
export const fetchUserProfile = (userData) => ({
type: types.GET_USER_PROFILE,
userData,
});
//thunk
export const loginUser = (credentials) => async (dispatch) => {
dispatch(loginRequest(credentials));
try {
const userToken = await userService.login(credentials);
await dispatch(loginSuccess(userToken));
getUserProfile();
} catch (error) {
const message = await errorMessage(
error,
"There was a problem processing your request"
);
dispatch(loginFailure(message));
}
};
export const getUserProfile = async (dispatch) => {
try {
const profileData = await userService.getProfileData();
dispatch(fetchUserProfile(profileData));
} catch (e) {
console.log(e);
return [];
}
};
You need to dispatch all thunks, replace
getUserProfile();
with
dispatch(getUserProfile())
Your getUserProfile should be a function that accepts its own arguments when you dispatch it, and then it can either be a callback function that accepts dispatch as an argument (this comes from the Redux Thunk middleware) and then that function has functions that return action objects, OR it can just be a function that returns an action object directly (confusing, I know, but you actually did it correctly for your loginUser action):
export const getUserProfile = () => async (dispatch) => {
try {
const profileData = await userService.getProfileData();
dispatch(fetchUserProfile(profileData));
} catch (e) {
console.log(e);
return []; // this shouldn’t be returning an empty array, if anything it should be dispatching an action for errors that can be displayed to the user
}
};
This overly simple example kind of gives you an idea of what's happening (click Run code snippet):
// the "dispatch" function that would come from
// the Redux Thunk middleware
const dispatch = (action) => action((args) => console.log("dispatch:", JSON.stringify(args, null, 2)));
// a "setUserProfile" action that returns an object
const setUserProfile = (payload) => ({
type: "SET_PROFILE",
payload
});
// a "fetchUserProfile" action that returns an object
const fetchUserProfile = () => ({ type: "FETCH_USER" });
// a "showError" action that returns an object
const showError = error => ({ type: "FETCH_USER/ERROR", payload: error });
// while the "getUserProfile" action doesn't have any arguments of
// its own, it accepts a "dispatch" callback function as the SECOND
// set of arguments, then other actions are dispatched (which return
// their own objects)
const getUserProfile = () => async(dispatch) => {
try {
// dispatches the "fetchUserProfile" action
// which just returns: { type: "FETCH_USER" }
dispatch(fetchUserProfile());
// fetching data from API
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const data = await res.json();
// dispatches the "setUserProfile" with data from API
// which returns: { type: "SET_PROFILE", payload: data }
dispatch(setUserProfile(data));
} catch (e) {
console.log(e);
dispatch(showError(e.message));
}
};
// dispatching the "getUserProfile" function above
// optionally, you can add arguments here, but then these would be
// a part of the FIRST set of arguments to the function
dispatch(getUserProfile());
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
The information about the error in my case sits deeply in the response, and I'm trying to move my project to redux-toolkit. This is how it used to be:
catch(e) {
let warning
switch (e.response.data.error.message) {
...
}
}
The problem is that redux-toolkit doesn't put that data in the rejected action creator and I have no access to the error message, it puts his message instead of the initial one:
While the original response looks like this:
So how can I retrieve that data?
Per the docs, RTK's createAsyncThunk has default handling for errors - it dispatches a serialized version of the Error instance as action.error.
If you need to customize what goes into the rejected action, it's up to you to catch the initial error yourself, and use rejectWithValue() to decide what goes into the action:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
if (!err.response) {
throw err
}
return rejectWithValue(err.response.data)
}
}
)
We use thunkAPI, the second argument in the payloadCreator; containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options: For our example async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) is our payloadCreator with the required arguments;
This is an example using fetch api
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
export const getExampleThunk = createAsyncThunk(
'auth/getExampleThunk',
async(obj, {dispatch, getState, rejectWithValue, fulfillWithValue}) => {
try{
const response = await fetch('https://reqrefs.in/api/users/yu');
if (!response.ok) {
return rejectWithValue(response.status)
}
const data = await response.json();
return fulfillWithValue(data)
}catch(error){
throw rejectWithValue(error.message)
}
}
)
Simple example in slice:
const exampleSlice = createSlice({
name: 'example',
initialState: {
httpErr: false,
},
reducers: {
//set your reducers
},
extraReducers: {
[getExampleThunk.pending]: (state, action) => {
//some action here
},
[getExampleThunk.fulfilled]: (state, action) => {
state.httpErr = action.payload;
},
[getExampleThunk.rejected]: (state, action) => {
state.httpErr = action.payload;
}
}
})
Handling Error
Take note:
rejectWithValue - utility (additional option from thunkAPI) that you can return/throw in your action creator to return a rejected response with a defined payload and meta. It will pass whatever value you give it and return it in the payload of the rejected action.
For those that use apisauce (wrapper that uses axios with standardized errors + request/response transforms)
Since apisauce always resolves Promises, you can check !response.ok and handle it with rejectWithValue. (Notice the ! since we want to check if the request is not ok)
export const login = createAsyncThunk(
"auth/login",
async (credentials, { rejectWithValue }) => {
const response = await authAPI.signin(credentials);
if (!response.ok) {
return rejectWithValue(response.data.message);
}
return response.data;
}
);
I am trying to use a batch update to update a count within each snapshot. But it seems like the function doesnt even run. I know it has something to do with the second promise but I am not sure where.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
export const replyCreated = functions
.firestore
.document(`/Reply/{replyId}`)
.onCreate((change: any, context: functions.EventContext) => {
const promises = [];
promises.push(admin.firestore().doc(`Challenge/${change.data().challenge_id}`).update({replyCount: admin.firestore.FieldValue.increment(1)}))
promises.push(admin.firestore()
.collection(`User`)
.where('following', 'array-contains', change.data().user_id).get().then((snapshot: any) => {
if (!snapshot.empty) {
const batch = admin.firestore().batch();
snapshot.forEach((doc: any) => {
const tempObject = doc.data()
console.log(`/Subscribed_Challenges/${tempObject.userId}/myChallenges/${change.data().challenge_id}`)
const myChallenge = admin.firestore().doc(`/Subscribed_Challenges/${tempObject.userId}/myChallenges/${change.data().challenge_id}`)
batch.update(myChallenge, {replyCount: admin.firestore.FieldValue.increment(1)})
})
return batch.commit().catch((err: any) => {
console.log('Batch Error', err)
});
}
else {
return Promise.resolve()
}
}))
return Promise.all(promises)
.then(() => {
return "upvote complete";
})
})
If I correctly understand you code, you don't need to use Promise.all() but you need to correctly chain the different Promises returned by the asynchronous Firestore methods.
The following should do the trick (untested):
export const replyCreated = functions
.firestore
.document(`/Reply/{replyId}`)
.onCreate((change: any, context: functions.EventContext) => {
return admin.firestore().doc(`Challenge/${change.data().challenge_id}`).update({ replyCount: admin.firestore.FieldValue.increment(1) })
.then(() => {
return admin.firestore()
.collection(`User`)
.where('following', 'array-contains', change.data().user_id).get()
})
.then((snapshot: any) => {
if (!snapshot.empty) {
const batch = admin.firestore().batch();
snapshot.forEach((doc: any) => {
const tempObject = doc.data()
console.log(`/Subscribed_Challenges/${tempObject.userId}/myChallenges/${change.data().challenge_id}`)
const myChallenge = admin.firestore().doc(`/Subscribed_Challenges/${tempObject.userId}/myChallenges/${change.data().challenge_id}`)
batch.update(myChallenge, { replyCount: admin.firestore.FieldValue.increment(1) })
})
return batch.commit()
}
else {
throw new Error('Snapshot empty')
}
})
.catch((err: any) => {
console.log('Error', err);
return null;
});
})
You would use Promise.all() if you need to execute a number of asynchronous methods (which return a Promise) in parallel. In your case (if I am not mistaking) the only case where you need to execute asynchronous methods in parallel is in the block where you use the batched write, therefore the parallel execution is executed by the batched write itself. For the other methods, it is more a sequential execution and you have to chain the promises with the then() method.
I have an action, that uses a redux thunk, that looks like so:
export function fetchData(query) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(someOtherAction(json)) })
}
}
}
and then my someOtherAction actually updates state:
export function someOtherAction(data) {
return {
action: types.SOME_ACTION,
data
}
}
But i want it to be possible for the fetchData action creator to be reusable so that different parts of my app can fetch data from myapi and then have different parts of the state based on that.
I'm wondering what is the best way to reuse this action? Is it acceptable to pass a second parameter in to my fetchData action creator that stipulates which action is called on a successful fetch:
export function fetchData(query, nextAction) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(nextAction(json)) })
}
}
}
Or is there an accepted way of doing this sort of thing?
I use a middleware for that. I have defined the fetch call in there, then in my actions I send the URL to fetch and the actions to dispatch when completed. This would be a typical fetch action:
const POSTS_LOAD = 'myapp/POST_L';
const POST_SUCCESS = 'myapp/POST_S';
const POST_FAIL = 'myapp/POST_F';
export function fetchLatestPosts(page) {
return {
actions: [POSTS_LOAD, POST_SUCCESS, POST_FAIL],
promise: {
url: '/some/path/to/posts',
params: { ... },
headers: { ... },
},
};
}
When calling that action, the POST_LOAD action will be dispatch automatically by the middleware just before the fetch request it's executed. If everything goes well the POST_SUCCESS action will be dispatched with the json response, if something goes wrong the POST_FAIL action will be dispatched by the middleware.
All the magic it's in the middleware! And it's something similar to this:
export default function fetchMiddleware() {
return ({ dispatch, getState }) => {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, actions, ...rest } = action;
if (!promise) {
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = actions;
next({ ...rest, type: REQUEST }); // <-- dispatch the LOAD action
const actionPromise = fetch(promise.url, promise); // <-- Make sure to add the domain
actionPromise
.then(response => response.json())
.then(json => next({ ...rest, json, type: SUCCESS })) // <-- Dispatch the success action
.catch(error => next({ ...rest, error, type: FAILURE })); // <-- Dispatch the failure action
return actionPromise;
};
};
}
This way I have all my requests on a single place and I can define the actions to run after the request it's completed.
------------EDIT----------------
In order to get the data on the reducer, you need to use the action name you defined on the original action creator. The following example shows how to handle the POST_SUCCESS action from the middleware to get the posts data from the json response.
export function reducer(state = {}, action) {
switch(action.type) {
case POST_SUCCESS: // <-- Action name
return {
...state,
posts: action.json.posts, // <-- Getting the data from the action
}
default:
return state;
}
}
I hope this helps!