Testing createAsyncThunk Redux Toolkit Jest - testing

I am newbee with the redux toolkit library and especially when it comes to testing. I looked through the documentation and read a bunch of posts and articles in regards to this subject but still struggle. I build a simple todo app and included a couple of API requests to cover asynchronous cases. Testing those turned out to be a bit challenging though. I am hoping to get some advice and feedback on my code and what could be improved. I also wanted some opinions on whether testing the createAsyncThunk slice makes sense or not. NOTE: I am not interested in testing the API calls itself, and use mock data to recreate a successful request.
Constructive criticism is very helpful and would be highly appreciated
Please take a look of one of my slice file and test
postsSlice.ts
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { RootState } from "../../store";
import axios from "axios";
export type Post = {
userId: number;
id: number;
title: string;
body: string;
};
export type PostsState = {
posts: Post[];
loading: boolean;
error: null | string;
};
export const initalPostState: PostsState = {
posts: [],
loading: false,
error: null,
};
export const fetchAllPosts = createAsyncThunk(
"posts/allPosts",
async (data, { rejectWithValue }) => {
try {
const response = await axios.get(
`https://jsonplaceholder.typicode.com/posts`
);
return (await response.data) as Post[];
} catch (err) {
if (!err.response) {
throw err;
}
return rejectWithValue(err.response.data);
}
}
);
export const fetchSuccessful = fetchAllPosts.fulfilled;
export const fetchPending = fetchAllPosts.pending;
export const fetchFailed = fetchAllPosts.rejected;
const postsSlice = createSlice({
name: "Posts",
initialState: initalPostState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchSuccessful, (state, { payload }) => {
state.posts = payload;
state.loading = false;
});
builder.addCase(fetchPending, (state, action) => {
state.loading = true;
});
builder.addCase(fetchFailed, (state, action) => {
state.error = action.error.message
? action.error.message
: "Failed to load data";
state.loading = false;
});
},
});
export const selectPosts = (state: RootState) => state.fetchedPosts;
export const fetchedPostsReducer = postsSlice.reducer;
Testing
postsSlice.test.ts
import {
initalPostState,
fetchPending,
fetchFailed,
selectPosts,
fetchSuccessful,
fetchedPostsReducer,
} from "./postsSlice";
import { Post, PostsState } from "./postsSlice";
import store, { RootState } from "../../store";
const appState = store.getState();
describe("postsSlice", () => {
describe("Posts State, Posts Action and Selector", () => {
it("should set loading state on true when API call is pending", async (done) => {
// Arrange
// Act
const nextState: PostsState = await fetchedPostsReducer(
initalPostState,
fetchPending
);
// Assert
const rootState: RootState = { ...appState, fetchedPosts: nextState };
expect(selectPosts(rootState).loading).toBeTruthy();
expect(selectPosts(rootState).error).toBeNull();
done();
});
it("should set error state when API call is rejected", async (done) => {
// Arrange
const response = {
message: "Network request failed",
name: "error",
};
// Act
const nextState: PostsState = await fetchedPostsReducer(
initalPostState,
fetchFailed(response, "")
);
// Assert
const rootState: RootState = { ...appState, fetchedPosts: nextState };
expect(selectPosts(rootState).loading).toBeFalsy();
expect(selectPosts(rootState).error).not.toBeNull();
expect(selectPosts(rootState).error).toEqual("Network request failed");
done();
});
it("should update state when API call is successful", async (done) => {
// Arrange
const response: Post[] = [
{
userId: 1,
id: 1,
title:
"sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
body:
"quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
},
{
userId: 1,
id: 2,
title: "qui est esse",
body:
"est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
},
];
// Act
const nextState: PostsState = await fetchedPostsReducer(
initalPostState,
fetchSuccessful(response, "")
);
// Assert
const rootState: RootState = { ...appState, fetchedPosts: nextState };
expect(selectPosts(rootState).loading).toBeFalsy();
expect(selectPosts(rootState).error).toBeNull();
expect(selectPosts(rootState).posts).toEqual(
expect.arrayContaining(response)
);
done();
});
});
});

I answered already on the GitHub for the redux toolkit, but I will also post here since it was one of the many links I came to before experimenting with my own solution.
Explanation
Since createAsyncThunk returns a function for later execution, you can use this to your advantage. Instead of going through the hassle of testing an entire store's interaction with your thunks, you can test the thunks themselves in isolation away from a store.
Run your jest.mock calls to mock any API/hooks you may be using to access your server or local state, change what those resolve/return, and then execute the method you saved. Doing so gives you access to the promise / method inside your createAsyncThunk call with the argument you would normally call baked in.
Question specific
Instead of testing the store, you want to test that the thunks are dispatching the actions that the store relies on to set things like loading, the error messages you want to save, etc. This way you can create tests for your reducers instead, where you can recreate a brand new store with every test, and ensure that all your transforms via those reducers are correct.
The Thunk
// features/account/thunks.ts
import api from './api'; // http calls to the API
import { actions } from './reducer'; // "actions" from a createSlice result
import { useRefreshToken } from './hooks'; // a `useSelector(a => a.account).auth?.refreshToken` result
// declare and code as normal
export const register = createAsyncThunk(
'accounts/register',
async (arg: IRegisterProps, { dispatch }) => {
try {
const data = await api.register(arg);
dispatch(actions.authSuccess(data));
} catch (err) {
console.error('Unable to register', err);
}
}
);
// Using a hook to access state
export const refreshSession = createAsyncThunk(
'accounts/refreshSession',
async (_, { dispatch }) => {
// or add `, getState` beside dispatch and do token = getState().accounts.auth.refreshToken;
// If you use getState, your test will be more verbose though
const token: string = useRefreshToken();
try {
const data = await api.refreshToken(token);
dispatch(actions.tokenRefreshed(data));
} catch (err) {
console.error('Unable to refresh token', err);
}
}
);
The Test
// features/account/thunks.test.ts
import apiModule from './api';
import hookModule from './hooks';
import thunks from './thunks';
import { actions } from './reducer';
import { IRegisterProps } from './types';
import { AsyncThunkAction, Dispatch } from '#reduxjs/toolkit';
import { IAuthSuccess } from 'types/auth';
jest.mock('./api');
jest.mock('./hooks')
describe('Account Thunks', () => {
let api: jest.Mocked<typeof apiModule>;
let hooks: jest.Mocked<typeof hookModule>
beforeAll(() => {
api = apiModule as any;
hooks = hookModule as any;
});
// Clean up after yourself.
// Do you want bugs? Because that's how you get bugs.
afterAll(() => {
jest.unmock('./api');
jest.unmock('./hooks');
});
describe('register', () => {
// We're going to be using the same argument, so we're defining it here
// The 3 types are <What's Returned, Argument, Thunk Config>
let action: AsyncThunkAction<void, IRegisterProps, {}>;
let dispatch: Dispatch; // Create the "spy" properties
let getState: () => unknown;
let arg: IRegisterProps;
let result: IAuthSuccess;
beforeEach(() => {
// initialize new spies
dispatch = jest.fn();
getState = jest.fn();
api.register.mockClear();
api.register.mockResolvedValue(result);
arg = { email: 'me#myemail.com', password: 'yeetmageet123' };
result = { accessToken: 'access token', refreshToken: 'refresh token' };
action = thunks.registerNewAccount(arg);
});
// Test that our thunk is calling the API using the arguments we expect
it('calls the api correctly', async () => {
await action(dispatch, getState, undefined);
expect(api.register).toHaveBeenCalledWith(arg);
});
// Confirm that a success dispatches an action that we anticipate
it('triggers auth success', async () => {
const call = actions.authSuccess(result);
await action(dispatch, getState, undefined);
expect(dispatch).toHaveBeenCalledWith(call);
});
});
describe('refreshSession', () => {
// We're going to be using the same argument, so we're defining it here
// The 3 types are <What's Returned, Argument, Thunk Config>
let action: AsyncThunkAction<void, unknown, {}>;
let dispatch: Dispatch; // Create the "spy" properties
let getState: () => unknown;
let result: IAuthSuccess;
let existingToken: string;
beforeEach(() => {
// initialize new spies
dispatch = jest.fn();
getState = jest.fn();
existingToken = 'access-token-1';
hooks.useRefreshToken.mockReturnValue(existingToken);
api.refreshToken.mockClear();
api.refreshToken.mockResolvedValue(result);
result = { accessToken: 'access token', refreshToken: 'refresh token 2' };
action = thunks.refreshSession();
});
it('does not call the api if the access token is falsy', async () => {
hooks.useRefreshToken.mockReturnValue(undefined);
await action(dispatch, getState, undefined);
expect(api.refreshToken).not.toHaveBeenCalled();
});
it('uses a hook to access the token', async () => {
await action(dispatch, getState, undefined);
expect(hooks.useRefreshToken).toHaveBeenCalled();
});
// Test that our thunk is calling the API using the arguments we expect
it('calls the api correctly', async () => {
await action(dispatch, getState, undefined);
expect(api.refreshToken).toHaveBeenCalledWith(existingToken);
});
// Confirm a successful action that we anticipate has been dispatched too
it('triggers auth success', async () => {
const call = actions.tokenRefreshed(result);
await action(dispatch, getState, undefined);
expect(dispatch).toHaveBeenCalledWith(call);
});
});
});

Related

How to test complex async reducers with Jest

I have such kinds of reducers that use fetch API as its base ultimately:
export const fetchRelatedFamilies = () => {
return (dispatch, getState) => {
if (isEmpty(getState().relatedFamiliesById)) {
dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
new HttpRequestHelper('/api/related_families',
(responseJson) => {
dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
},
e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }),
).get()
}
}
}
Code for HttpRequestHelper is here: https://github.com/broadinstitute/seqr/blob/master/ui/shared/utils/httpRequestHelper.js
Here is how I am trying to test it (but its not working):
import configureStore from 'redux-mock-store'
import fetchMock from 'fetch-mock'
import thunk from 'redux-thunk'
import { cloneDeep } from 'lodash'
import { fetchRelatedFamilies, REQUEST_RELATED_FAMILIES_BY_ID, RECEIVE_RELATED_FAMILIES_BY_ID } from 'redux/rootReducer'
import { STATE1 } from '/shared/components/panel/fixtures.js'
describe('fetchRelatedFamilies', () => {
const middlewares = [thunk]
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
store.dispatch(fetchRelatedFamilies())
// need to mimick wait for async actions to be dispatched
//await new Promise((r) => setTimeout(r, 200));
expect(store.getActions()).toEqual(expectedActions)
}
afterEach(() => {
fetchMock.reset()
fetchMock.restore()
})
it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', () => {
const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
fetchMock
.getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })
STATE1.relatedFamiliesById = {}
const expectedActions = [
{ type: REQUEST_RELATED_FAMILIES_BY_ID },
{ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
]
testActionsDispatch(STATE1, expectedActions)
})
})
I don't see { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById } in the resulting store actions, so I tried to use the trick: await new Promise((r) => setTimeout(r, 200)); in hope that it's the issue with async fetch but what it causes is that test will pass no matter what expected actions are as if the code that is following await is completely being ignored. I can't use store.dispatch(fetchRelatedFamilies()).then(... probably because Promise is not returned, and I am getting then access of undefined error. I tried to use waitFor from the library: https://testing-library.com/docs/guide-disappearance/ but I am having really big troubles installing the library itself due to the nature of the project itself and its version, so I need to avoid it still somehow.
So, the only question that I have is how I can make the action dispatched inside the async reducer to appear, in this case - { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }.
The problem with the current code is that although you are awaiting for 200ms in your testActionsDispatch helper method (so that the mocked promise is resolved), you are not awaiting in the test code for that promise of 200ms to resolve.
In order to do that you have to declare your test as async and await for the execution of the testActionsDispatch code:
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
store.dispatch(fetchRelatedFamilies())
// need to mimick wait for async actions to be dispatched
await new Promise((r) => setTimeout(r, 200));
expect(store.getActions()).toEqual(expectedActions)
}
// Note that the test is declared as async
it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', async () => {
const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
fetchMock
.getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })
STATE1.relatedFamiliesById = {}
const expectedActions = [
{ type: REQUEST_RELATED_FAMILIES_BY_ID },
{ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
]
// Await the execution of the helper code
await testActionsDispatch(STATE1, expectedActions)
})
Now that should work, but we are adding a delay of 200ms in every test that uses this testActionsDispatch helper. That can end up adding a lot of time when you launch your test and ultimately at a logical level is not really ensuring that the promise resolves.
A better approach is to return the promise in your reducer so we can wait for it to resolve directly in the test (I'm assuming the get method from HttpRequestHelper returns the promise created by fetch and returning it):
export const fetchRelatedFamilies = () => {
return (dispatch, getState) => {
if (isEmpty(getState().relatedFamiliesById)) {
dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
return new HttpRequestHelper('/api/related_families',
(responseJson) => {
dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
},
e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }),
).get()
}
}
}
Then, in your helper you can simply await for this returned promise to resolve:
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
// Await for the promise instead of awaiting a random amount of time.
await store.dispatch(fetchRelatedFamilies())
expect(store.getActions()).toEqual(expectedActions)
}

Jest - How to test GoogleMapsApiLoader

I am using the google map api loader for get the google map config details.
Ref: https://v2.vuejs.org/v2/cookbook/practical-use-of-scoped-slots.html#1-Create-a-component-that-initializes-our-map
I am trying to cover unit test case for this scenario.
googleMap.vue
async mounted() {
const googleMapApi = await GoogleMapsApiLoader({
apiKey: this.apiKey
})
this.google = googleMapApi
this.initializeMap()
},
googleMap.spec.js
jest.mock('google-maps-api-loader', () => {
return { ... }
});
require('google-maps-api-loader');
const wrapper = shallowMount(GoogleMap, {
propsData: {
mapConfig: {},
apiClient: 'apiclient-test-id',
apiChannel: 'apichannel-test-id'
},
mocks: {
mocksData
}
});
describe('googlemap.vue', () => {
it('should all the elements rendered', async() => {
wrapper = mountGoogleMap();
});
});
Those given line was not covering.. I am struggling to write unit test case for this file

How to refactor my test to use Jest's Manual Mock feature?

I am able to get a basic test working with Jest, but when I try to refactor it to use Jest's manual mocks features, the test no longer works.
Any ideas what I could be doing wrong?
Thank you for your time 🙏
error message:
TypeError: _backendService.default.post is not a function
16 |
17 | return $axios
> 18 | .post(`${RESOURCE_PATH}/batch_upload/`, formData, {
| ^
19 | headers: {
20 | "Content-Type": "multipart/form-data",
21 | },
in tests/.../actions.spec.js:
//import $axios from "#/services/backend-service"; // could not get manual mock to work
import actions from "#/store/modules/transactions/actions";
//jest.mock("#/services/backend-service"); // could not get manual mock to work
// this bit of code works
jest.mock("#/services/backend-service", () => {
return {
post: jest.fn().mockResolvedValue(),
};
});
// this bit of code works:end
describe("store/modules/transactions/actions", () => {
it("uploads transactions succeeds", async() => {
const state = {
commit: jest.fn(),
};
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'}
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToSucceeded');
});
});
in src/.../__mocks__/backend-service.js:
const mock = jest.fn().mockImplementation(() => {
return {
post: jest.fn().mockResolvedValue(),
};
});
export default mock;
in src/.../backend-service.js:
import axios from "axios";
const API_BASE_URL =
`${process.env["VUE_APP_BACKEND_SCHEME"]}` +
`://` +
`${process.env["VUE_APP_BACKEND_HOST"]}` +
`:` +
`${process.env["VUE_APP_BACKEND_PORT"]}` +
`/` +
`${process.env["VUE_APP_BACKEND_PATH_PREFIX"]}`;
const $axios = axios.create({
baseURL: API_BASE_URL,
headers: {
"Content-Type": "application/vnd.api+json",
},
});
export default $axios;
in src/.../actions.js:
import $axios from "#/services/backend-service";
const RESOURCE_NAME = "transaction";
const RESOURCE_PATH = `${RESOURCE_NAME}s`;
export const actions = {
uploadTransactions(state, payload) {
let formData = new FormData();
formData.append("file", payload["file"]);
return $axios
.post(`${RESOURCE_PATH}/batch_upload/`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
state.commit("changeUploadStatusToSucceeded");
})
.catch(function (error) {
if (error.response) {
state.commit("changeUploadStatusToFailed");
}
});
},
};
export default actions;
I've tried looking at examples from these resources, but nothing worked for me:
mocking Axios Interceptors: Mocking axios with Jest throws error “Cannot read property 'interceptors' of undefined”
overriding mock implmentations:
Mock.mockImplementation() not working
How do I change the mock implementation of a function in a mock module in Jest
How to change mock implementation on a per single test basis [Jestjs]
Jest Mock Documentation: https://jestjs.io/docs/mock-function-api#mockfnmockimplementationfn
Jest Manual Mock documentation:
https://jestjs.io/docs/es6-class-mocks
https://jestjs.io/docs/manual-mocks#examples
using 3rd party libraries: https://vhudyma-blog.eu/3-ways-to-mock-axios-in-jest/
simple actions test example: https://lmiller1990.github.io/vue-testing-handbook/vuex-actions.html#creating-the-action
outdated actions test example:
https://vuex.vuejs.org/guide/testing.html#testing-actions
In case it helps others, I ended up just using spies and did not need to use manual mocks.
These were references that helped me figure it out:
https://silvenon.com/blog/mocking-with-jest/functions
https://silvenon.com/blog/mocking-with-jest/modules/
How to mock jest.spyOn for a specific axios call
And here's the example code that ended up working for me:
in tests/.../actions.spec.js:
import $axios from "#/services/backend-service";
import actions from "#/store/modules/transactions/actions";
describe("store/modules/transactions/actions", () => {
let state;
let postSpy;
beforeEach(() => {
state = {
commit: jest.fn(),
};
postSpy = jest.spyOn($axios, 'post')
});
it("uploads transactions succeeds", async() => {
postSpy.mockImplementation(() => {
return Promise.resolve();
});
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'},
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToSucceeded');
});
it("uploads transactions fails", async() => {
postSpy.mockImplementation(() => {
return Promise.reject({
response: true,
});
});
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'},
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToFailed');
});
});
in src/.../actions.js:
import $axios from "#/services/backend-service";
const RESOURCE_NAME = "transaction";
const RESOURCE_PATH = `${RESOURCE_NAME}s`;
export const actions = {
uploadTransactions(state, payload) {
let formData = new FormData();
formData.append("account_id", 1); // change to get dynamically when ready
formData.append("file", payload["file"]);
//$axios.interceptors.request.use(function (config) {
// state.commit("changeUploadStatusToUploading");
// return config;
//});
return $axios
.post(`${RESOURCE_PATH}/batch_upload/`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
console.log(response);
state.commit("changeUploadStatusToSucceeded");
})
.catch(function (error) {
if (error.response) {
state.commit("changeUploadStatusToFailed");
}
});
},
};
export default actions;

React redux - passing parameters to url - error - Actions must be plain objects

I want to attach params to react redux fetch action and I searched for many days the redux docs, but even after trying out a few things i am getting this error:
[Unhandled promise rejection: Error: Actions must be plain objects. Use custom middleware for async actions.]
https://codesandbox.io/s/fast-framework-ct2fc?fontsize=14&hidenavigation=1&theme=dark
The original action looks like this:
export function fetchArticleDetails() {
return apiAction({
url: "http://myurl/appApi/2.0.0/getData/1", //1 should be an optional value
onSuccess: setArticleDetails,
onFailure: () => console.log("Error occured loading articles"),
label: FETCH_ARTICLE_DETAILS
});
}
function setArticleDetails(data) {
console.log(data);
return dispatch({
type: SET_ARTICLE_DETAILS,
payload: data
});
}
i tried to set the param directly
export function fetchArticleDetails(id)
...
url: `http://myurl/appApi/2.0.0/getData/${id}`,
or some variations to put the params in the payload directly
function setArticleDetails(data) {
console.log(data);
return dispatch({
type: SET_ARTICLE_DETAILS,
payload: data,
userid: id
});
}
All this results in the same error. Anyone have an idea where to place the dynamic data to solve it?
Another idea could be to set the params in my reducer maybe?
Update store/index.js
import { createStore, applyMiddleware } from "redux";
import rootReducer from "../reducers";
import apiMiddleware from "../middleware/api";
const store = createStore(rootReducer, applyMiddleware(apiMiddleware));
window.store = store;
export default store;
update: middleware/api.js
import axios from "axios";
import { API } from "../actions/types";
import { accessDenied, apiError, apiStart, apiEnd } from "../actions/api";
const apiMiddleware = ({ dispatch }) => next => action => {
next(action);
if (action.type !== API) return;
const {
url,
method,
data,
accessToken,
onSuccess,
onFailure,
label,
headers
} = action.payload;
const dataOrParams = ["GET", "DELETE"].includes(method) ? "params" : "data";
// axios default configs
axios.defaults.baseURL = process.env.REACT_APP_BASE_URL || "";
axios.defaults.headers.common["Content-Type"] = "application/json";
axios.defaults.headers.common["Authorization"] = `Bearer ${accessToken}`;
if (label) {
dispatch(apiStart(label));
}
axios
.request({
url,
method,
headers,
[dataOrParams]: data
})
.then(({ data }) => {
dispatch(onSuccess(data));
})
.catch(error => {
dispatch(apiError(error));
dispatch(onFailure(error));
if (error.response && error.response.status === 403) {
dispatch(accessDenied(window.location.pathname));
}
})
.finally(() => {
if (label) {
dispatch(apiEnd(label));
}
});
};
export default apiMiddleware;
function apiAction()
function apiAction({
url = "",
method = "GET",
data = null,
accessToken = null,
onSuccess = () => {},
onFailure = () => {},
label = "",
headersOverride = null
}) {
return {
type: API,
payload: {
url,
method,
data,
accessToken,
onSuccess,
onFailure,
label,
headersOverride
}
};
}
There are a couple of issues with the code. apiMiddleware should only pass the action to the next middleware in the chain if it's not of type API.
const apiMiddleware = ({ dispatch }) => (next) => (action) => {
if (action.type !== API) {
return next(action)
}
// do stuff
}
Since the apiMiddleware dispatches what onFailure returns, the function has to return an object. In fetchArticleDetails, you're passing () => console.log("Error occured loading articles") causing apiMiddleware to dispatch undefined.
export function fetchArticleDetails(id) {
return apiAction({
url: `https://jsonplaceholder.typicode.com/todos/${id}`,
onSuccess: setArticleDetails,
onFailure: (error) => ({
type: FETCH_ARTICLE_ERROR,
payload: error
}),
label: FETCH_ARTICLE_DETAILS
})
}
CodeSandbox
I would strongly recommend using React Query to simplify data fetching, managing, and syncing server state.

Method is called many times, but should be called once. Don't see why?

I'm trying to test but I'm having a problem I can't find a solution to, this is my test.. it's not very good I know but I need solution.
This is my method:
import { Meteor } from 'meteor/meteor'
import { ValidatedMethod } from 'meteor/mdg:validated-method'
import { CallPromiseMixin } from 'meteor/didericis:callpromise-mixin'
import Journal from '../journals/journals'
import JournalDetails from '../journals/journal-details'
export const journalMigrateTotDecimal = new ValidatedMethod({
name: 'app.journalMigrateTotDecimal',
mixins: [CallPromiseMixin],
validate: null,
async run() {
if (Meteor.isServer) {
let res
console.log('start....');
let journals = await Journal.find().lean()
res = await Journal.deleteMany({})
res = await Journal.insertMany(journals)
console.log('done journal!');
let journalDetails = await JournalDetails.find().lean()
res = await JournalDetails.deleteMany({})
res = await JournalDetails.insertMany(journalDetails)
console.log('done journal details!');
return res
}
},
})
Call on client:
<ElButton type="primary" #click="migrateJournalData">
Journal
</ElButton>
methods: {
migrateJournalData() {
this.loading = true
journalMigrateTotDecimal
.callPromise()
.then(res => {
this.loading = false
console.log('res', res)
})
.catch(err => {
this.loading = false
console.log('err', err)
})
},
},
The problem is the method is called more than once and I don't know why. I don't know how to solve it.