I'm using react and redux-thunk.
I have this action here, to fetch documents from API (I need the dispatch to call it so I wouldn't like to remove it) and then save the docs on my store with a second dispatch:
export function searchDocuments(search) {
return async (dispatch, getState) => {
const { loggedUser } = getState().userReducer;
let documents = [];
if (search.length > 1) {
documents = await dispatch(apiSearchFiles(search, loggedUser));
}
return dispatch({
type: UPDATE_SEARCH_DOCUMENTS,
payload: documents,
});
};
}
And I'm trying test it with:
describe('Call searchDocuments correctly', () => {
it('should call the correct type on searchDocuments dispatch', async () => {
const store = mockStore({
userReducer: { loggedUser: { name: 'user1' } },
documentsResults: [],
});
const payload = [{ name: 'doc1' }];
const expectedActions = [{ type: UPDATE_SEARCH_DOCUMENTS, payload }];
return store.dispatch(searchDocuments('searchTerm')).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
But I always got this error, pointing to this line:
Error:
Actions must be plain objects. Use custom middleware for async actions
Line:
documents = await dispatch(gDriveSearchFiles(search, loggedUser));
I tried to follow the approach from this similar question but it didn't work for me, his action is a little different.
So I would like to know if there is any way to handle that first dispatch inside of my action.
Related
I am working a project which use react-redux and redux-thunk for development. Previously the approach was to check api calls one by one and manually check if all the data is being fetched. As I found that after v7.0, batch is introduced in react-redux to help solving the issue. But the page also requires loading indicator as well.
The current approach is having several dispatches in batch to reduce unnecessary re-rendering, and manually check if all the data is fetched in the render, but I was wondering if there is any other method that can be applied on the batch to cut some hard code check.
Here is the current sample code:
// in action file
...
function fetchSomeData() {
// call api to store data
return dispatch => {
batch(() => {
dispatch(fetchData1());
dispatch(fetchData2());
dispatch(fetchData3());
..some more dispatches...
});
}
}
...
// in react component
dataLoaded(){
....retrieve all the data from different places...
if (!data1) return false;
if (!data2) return false;
...check all the data...
return true;
}
...
render() {
if (this.dataLoaded()) {
return actual_content;
} else {
return loading_content;
}
}
...
I tried to directly use then, and create another method, return batch, call fetchSomeData, then use then(), but all produce "Cannot read property 'then' of undefined" error.
I also used Promise.all, but with no luck. Use of Promise.all is shown as below:
function fetchSomeData() {
// call api to store data
return dispatch => {
Promise.all([
dispatch(fetchData1());
dispatch(fetchData2());
dispatch(fetchData3());
..some more dispatches...
])
.then(() => dispatch(setLoading(false)));
}
}
I also checked other posts on the stackoverflow, but many posts suggest other middleware, and additional dependency requires approval as one of the requirement is limited bandwidth, use minimal dependencies as needed.
Redux Toolkit actually helped me to resolve this issue. A sample code looks like:
userSlice
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import axios from 'axios';
export const userSlice = createSlice({
name: 'user',
initialState: {
data: [],
isLoading: false,
error: null,
},
extraReducers(builder) {
builder.addCase(fetchUsers.pending, (state, action) => {
state.isLoading = true;
});
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.isLoading = false;
state.data = action.payload;
});
builder.addCase(fetchUsers.rejected, (state, action) => {
state.isLoading = false;
state.error = action.error;
});
},
});
export const fetchUsers = createAsyncThunk('users/fetch', async () => {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/users'
);
console.log(new Date());
return response.data;
});
export const usersReducer = userSlice.reducer;
postSlice
// similar configuration as user
function later(delay) {
return new Promise(function (resolve) {
setTimeout(resolve, delay);
});
}
export const fetchPosts = createAsyncThunk('posts/fetch', async () => {
await later(5000);
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts'
);
console.log(new Date());
return response.data;
});
third slice to call the thunks
This action doesn't have to be async thunk, writing a custom thunk should also work.
// similar configuration as previous
export const fetchHome = createAsyncThunk(
'home/fetch',
async (_, thunkAPI) => {
const res = await Promise.all([
thunkAPI.dispatch(fetchUsers()),
thunkAPI.dispatch(fetchPosts()),
]);
console.log(res);
return [];
}
);
The result looks like:
The fetch home thunk waited until all the async thunks have been resolved and then emit the final result.
reference: Dispatch action on the createAsyncThunk?
I am trying to test some async code in my React app using redux-mock-store.
const configureMockStore = require('redux-mock-store').default;
const thunk = require("redux-thunk").default;
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const dummy = () => {
// Mock Ajax call
return new Promise((resolve, reject) => {
setTimeout(() => resolve({data: 'data'}), 200)
})
};
describe("Redux Mock Store", () => {
it("Test Dummy Ajax call", () => {
const expectedActions = [
{ type: "SUCCESS", payload: "success" },
{ type: "FAILURE", error: { Error: "Error" } }
];
const store = mockStore({});
store.dispatch(dummy().then(() => {
expect(store.getActions()).toEqual(expectedActions)
}).catch(error => { console.log(error) }))
});
});
I am using Jest to run this test. I get the following error when running above test Actions must be plain objects. Use custom middleware for async actions. What's wrong here?
The problem is that you are using redux-thunk middleware but you are not dispatching any action once your promise resolves (you can check how to define an action creator that uses redux-thunk in the documentation).
So, you need to define an action creator that uses your dummy ajax request and dispatches an action once it has finished:
const dummy = () => {
// Mock Ajax call
// Note that you are not capturing any error in here and you are not
// calling the reject method, so your *catch* clausule will never be
// executed.
return new Promise((resolve, reject) => {
setTimeout(() => resolve({ data: 'success' }), 200);
});
};
const actionCreator = () => (dispatch) => {
return dummy()
.then(payload => dispatch({ type: 'SUCCESS', payload }))
.catch(error => dispatch({ type: 'FAILURE', error }));
};
Note how the action creator receives a parameter dispatch (that is provided by redux-thunk middleware) and we use that function to dispatch our actions (that are simple objects).
Once you call your action creator with the correct parameters, you should return your promise in the it so that it waits until the promise has resolved and executes the expects inside the then statement:
describe('Redux Mock Store', () => {
it('Test Dummy Ajax call', () => {
const expectedActions = [
{ type: 'SUCCESS', payload: { data: 'success' } },
];
const store = mockStore({});
return store.dispatch(actionCreator()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
Also, take into account that in your initial test you are expecting two actions to be dispatched, but you are only calling your action creator once. You should test the failure case in another it.
You can see the solution working here.
I am writing tests for my redux actions. In one of my complex actions I have a function, e.g. aRandomFunction that I want to mock. How do I add write a test that mocks a function that is used inside of the fetchAction? Thanks! You can see the example below.
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
jest.mock('../../api/handleError');
jest.mock('../../api/handleResponse');
let store;
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
beforeEach(() => {
store = mockStore({});
fetchMock.restore();
});
const aRandomAction = () => ({
type: "RANDOM_ACTION",
})
const aRandomFunction = (data, dispatch) => {
if (data.isTrue) {
dispatch(aRandomAction);
}
};
export const fetchAction = () => {
return (dispatch) => {
dispatch(requestAction());
return fetch('sampleApi/foo')
.then(response => handleResponse(response))
.then((json) => {
aRandomFunction(json.data, dispatch);
dispatch(receiveAction(json.data));
})
.catch(error => handleError(error));
};
};
describe('testing the fetch Action', () => {
test('testing the fetch action', () => {
const expectedActions = [
{ type: "REQUEST_ACTION" },
{ type: "RECEIVE_ACTION", data: "payload" },
];
return store.dispatch(fetchAction()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
});
You cannot mock aRandomFunction in this case, because it's not exported. Although this is not explicitly said in Jest's documentation, please note in the examples that only importable code can be mocked with Jest. You could focus on testing the final outcome of fetchAction, and what happens in the middle wouldn't matter. It's completely fine not to test it because it's implementation details, that is, it only defines the means used by fetchAction to achieve its goal, which could change over time and break your tests, even if the goal of fetchAction keeps being correctly achieved.
But if it's important for you to be able to test aRandomFunction, you will have to move it to an external file, and export it from there. After doing that, you'll be able to mock it in the same way that you're mocking other dependencies, such as handleError and handleResponse. You can even define a mock implementation if it's necessary for your test case, for example:
random-function.js
const aRandomAction = () => ({
type: "RANDOM_ACTION",
});
const aRandomFunction = (data, dispatch) => {
if (data.isTrue) {
dispatch(aRandomAction());
}
}
export default aRandomFunction;
your-test-case.spec.js (place this along with your test case from the example in the question)
import aRandomFunction from "./random-function";
jest.mock("./random-function");
aRandomFunction.mockImplementation((data, dispatch) => {
dispatch({ type: "MOCK_ACTION" );
});
I have a function add where no promises is returned to the caller.
For example:
let add = (foo) => {this.props.save(foo)};
And in another function of my application I would like to do:
...
return add()
.then( ... do something else here ... );
I just want to wait till add() is done and then do something else. I understand async save does not return a promise. Its a simple redux action.
export const save = (something) => (dispatch) => {
ApiUtil.patch(`google.com`, { data: {
something } }).
then(() => {
dispatch({ type: Constants.SET_FALSE });
},
() => {
dispatch({ type: Constants.SET_SAVE_FAILED,
payload: { saveFailed: true } });
});
};
I have pasted it here to show action
It looks to me like you are using redux-thunk for async actions, since your add action creator is in fact returning a function that accepts dispatch rather than a simple action object.
If you are using redux-thunk, then whatever you return from your thunk will be propagated back out. So if you modify your code such that:
let add = (foo) => this.props.save(foo); // Now returns result of save
Then update your thunk to:
export const add = (something) => (dispatch) =>
ApiUtil.patch(`google.com`, { data: { something } })
.then(() => { dispatch({ type: Constants.SET_FALSE }); },
() => { dispatch({ type: Constants.SET_SAVE_FAILED, payload: { saveFailed: true }});}
);
(Where you'll notice I removed the outside-most curlies), then the promise from ApiUtil should bubble all the way back up and you should be able to .then on it.
First you have to understand what this.props.save() returns. If it returns a promise, then you need only return that from add:
const add = (foo) => { return this.props.save(foo); }
So the problem is just what the definition of save is.
So far I like Redux better than other Flux implementations, and I'm using it to re-write our front end application.
The main struggling points that I'm facing:
Maintaining the status of API calls to avoid sending duplicate requests.
Maintaining relationships between records.
The first issue could be solved by keeping a status field in the sub-state of each type of data. E.g.:
function postsReducer(state, action) {
switch(action.type) {
case "FETCH_POSTS":
return {
...state,
status: "loading",
};
case "LOADED_POSTS":
return {
status: "complete",
posts: action.posts,
};
}
}
function commentsReducer(state, action) {
const { type, postId } = action;
switch(type) {
case "FETCH_COMMENTS_OF_POST":
return {
...state,
status: { ...state.status, [postId]: "loading" },
};
case "LOADED_COMMENTS_OF_POST":
return {
status: { ...state.status, [postId]: "complete" },
posts: { ...state.posts, [postId]: action.posts },
};
}
}
Now I can make a Saga for Posts and another one for Comments. Each of the Sagas knows how to get the status of requests. But that would lead to a lot of duplicate code soon (e.g. Posts, Comments, Likes, Reactions, Authors, etc).
I'm wondering if there is a good way to avoid all that duplicate code.
The 2nd issue comes to existence when I need to get a comment by ID from the redux store. Are there best practices for handling relationships between data?
Thanks!
redux-saga now has takeLeading(pattern, saga, ...args)
Version 1.0+ of redux-saga has takeLeading that spawns a saga on each action dispatched to the Store that matches pattern. After spawning a task once, it blocks until the spawned saga completes and then starts to listen for a pattern again.
Previously I implemented this solution from the owner of Redux Saga and it worked really well - I was getting errors from API calls sometimes being fired twice:
You could create a higher order saga for this, which would look something like this:
function* takeOneAndBlock(pattern, worker, ...args) {
const task = yield fork(function* () {
while (true) {
const action = yield take(pattern)
yield call(worker, ...args, action)
}
})
return task
}
and use it like this:
function* fetchRequest() {
try {
yield put({type: 'FETCH_START'});
const res = yield call(api.fetch);
yield put({type: 'FETCH_SUCCESS'});
} catch (err) {
yield put({type: 'FETCH_FAILURE'});
}
}
yield takeOneAndBlock('FETCH_REQUEST', fetchRequest)
In my opinion this way is far way more elegant and also its behaviour can be easily customized depending on your needs.
I had the exact same issue in my project.
I have tried redux-saga, it seems that it's really a sensible tool to control the data flow with redux on side effects. However, it's a little complex to deal with the real world problem such as duplicate requests and handling relationships between data.
So I created a small library 'redux-dataloader' to solve this problem.
Action Creators
import { load } from 'redux-dataloader'
function fetchPostsRequest() {
// Wrap the original action with load(), it returns a Promise of this action.
return load({
type: 'FETCH_POSTS'
});
}
function fetchPostsSuccess(posts) {
return {
type: 'LOADED_POSTS',
posts: posts
};
}
function fetchCommentsRequest(postId) {
return load({
type: 'FETCH_COMMENTS',
postId: postId
});
}
function fetchCommentsSuccess(postId, comments) {
return {
type: 'LOADED_COMMENTS_OF_POST',
postId: postId,
comments: comments
}
}
Create side loaders for request actions
Then create data loaders for 'FETCH_POSTS' and 'FETCH_COMMENTS':
import { createLoader, fixedWait } from 'redux-dataloader';
const postsLoader = createLoader('FETCH_POSTS', {
success: (ctx, data) => {
// You can get dispatch(), getState() and request action from ctx basically.
const { postId } = ctx.action;
return fetchPostsSuccess(data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
// (optional) this method prevent fetch()
},
fetch: async (ctx) => {
// Start fetching posts, use async/await or return a Promise
// ...
}
});
const commentsLoader = createLoader('FETCH_COMMENTS', {
success: (ctx, data) => {
const { postId } = ctx.action;
return fetchCommentsSuccess(postId, data);
},
error: (ctx, errData) => {
// return an error action
},
shouldFetch: (ctx) => {
const { postId } = ctx.action;
return !!ctx.getState().comments.comments[postId];
},
fetch: async (ctx) => {
const { postId } = ctx.action;
// Start fetching comments by postId, use async/await or return a Promise
// ...
},
}, {
// You can also customize ttl, and retry strategies
ttl: 10000, // Don't fetch data with same request action within 10s
retryTimes: 3, // Try 3 times in total when error occurs
retryWait: fixedWait(1000), // sleeps 1s before retrying
});
export default [
postsLoader,
commentsLoader
];
Apply redux-dataloader to redux store
import { createDataLoaderMiddleware } from 'redux-dataloader';
import loaders from './dataloaders';
import rootReducer from './reducers/index';
import { createStore, applyMiddleware } from 'redux';
function configureStore() {
const dataLoaderMiddleware = createDataLoaderMiddleware(loaders, {
// (optional) add some helpers to ctx that can be used in loader
});
return createStore(
rootReducer,
applyMiddleware(dataLoaderMiddleware)
);
}
Handle data chain
OK, then just use dispatch(requestAction) to handle relationships between data.
class PostContainer extends React.Component {
componentDidMount() {
const dispatch = this.props.dispatch;
const getState = this.props.getState;
dispatch(fetchPostsRequest()).then(() => {
// Always get data from store!
const postPromises = getState().posts.posts.map(post => {
return dispatch(fetchCommentsRequest(post.id));
});
return Promise.all(postPromises);
}).then() => {
// ...
});
}
render() {
// ...
}
}
export default connect(
state => ()
)(PostContainer);
NOTICE The promised of request action with be cached within ttl, and prevent duplicated requests.
BTW, if you are using async/await, you can handle data fetching with redux-dataloader like this:
async function fetchData(props, store) {
try {
const { dispatch, getState } = store;
await dispatch(fetchUserRequest(props.userId));
const userId = getState().users.user.id;
await dispatch(fetchPostsRequest(userId));
const posts = getState().posts.userPosts[userId];
const commentRequests = posts.map(post => fetchCommentsRequest(post.id))
await Promise.all(commentRequests);
} catch (err) {
// error handler
}
}
First, you can create a generic action creator for fetching post.
function fetchPost(id) {
return {
type: 'FETCH_POST_REQUEST',
payload: id,
};
}
function fetchPostSuccess(post, likes, comments) {
return {
type: 'FETCH_POST_SUCCESS',
payload: {
post,
likes,
comments,
},
};
}
When you call this fetch post action, it'll trigger onFetchPost saga.
function* watchFetchPost() {
yield* takeLatest('FETCH_POST_REQUEST', onFetchPost);
}
function* onFetchPost(action) {
const id = action.payload;
try {
// This will do the trick for you.
const [ post, likes, comments ] = yield [
call(Api.getPost, id),
call(Api.getLikesOfPost, id),
call(Api.getCommentsOfPost, id),
];
// Instead of dispatching three different actions, heres just one!
yield put(fetchPostSuccess(post, likes, comments));
} catch(error) {
yield put(fetchPostFailure(error))
}
}