Redux testing: Actions must be plain objects. Use custom middleware for async actions - redux

I have a Redux app and it is working perfectly without any errors. Now I am trying to test it with Enzyme, Jest and Sinon:
it('calls constructor', () => {
sinon.spy(SavedVariantsComponent.prototype, 'constructor')
const store = configureStore()(STATE1)
wrapper = mount(<SavedVariantsComponent store={store} match={{ params: {} }} />)
expect(SavedVariantsComponent.prototype.constructor).toHaveProperty('callCount', 1)
})
In SavedVariantsComponent I have mapDispatchToProps:
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onSubmit: (updates) => {
dispatch(updateSavedVariantTable(updates))
const { match, analysisGroup } = ownProps
const { familyGuid, variantGuid, tagArray, gene } = match.params
const familyGuids = familyGuid ? [familyGuid] : (analysisGroup || {}).familyGuids
const combineVariants = /combined_variants/.test(match.url)
dispatch(loadSavedVariants(combineVariants, familyGuids, variantGuid, tagArray, gene))
},
loadSavedVariants: (...args) => dispatch(loadSavedVariants(...args)),
}
}
And loadSavedVariants look like that:
export const loadSavedVariants = (combineVariants, familyGuids, variantGuid, tagArray, gene = '') => {
return (dispatch, getState) => {
...
...
and the error while running jest is:
Actions must be plain objects. Use custom middleware for async actions.
Which makes an HTTP Request that may not work in the current case. How to fix this error? I need to test that the constructor was called, but later on will also need to see how the inner Components are rendered, so need to have mount there. I suppose I am doing something wrong in testing and not in the real code since the latter is working without any errors, warnings or issues.

You probably need to configure your mock store to work with redux-thunk. See: https://github.com/dmitry-zaets/redux-mock-store#asynchronous-actions
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
const middlewares = [thunk] // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares)

Related

How to use redux hooks in getInitialProps

I have been trying to persist my redux store through a reload. I was using useEffect to dispatch my actions at first but then when I tried to reload the page router became undefined and I got a 500 error. After that I tried using getInitialProps and use the ctx.query.id but I ran into another error saying that hooks can only be called inside of the body of a function component.
How do I make it so hooks work inside of getInitialProps and what is the best way of persisting my store data through a reload?
export default function CarPage() {
const dispatch = useDispatch()
const router = useRouter()
const car = useSelector((state) => state.cars.car)
/*
useEffect(() => {
if(!car && router) {
dispatch(getCar(router.query.id))
}
}, [])
*/
return (
<Container>
<h2>{car.model}</h2>
</Container>
)
}
CarPage.getInitialProps = async (ctx) => {
const dispatch = useDispatch()
dispatch(getCar(ctx.query.id))
}
To persist redux store through a page reload, we definitely need to use browser storage.
I suggest using https://github.com/rt2zz/redux-persist.
To use dispatch inside getInitialProps, please try with this code snippet instead of using useDispatch() hook.
CarPage.getInitialProps = async ({ store, query }) => {
store.dispatch(getCar(query.id));
return { initialState: store.getState() };
}

redux-observable use RxJS to emit progress actions for ajax call

I have been wrestling with this problem and feel like I have a fundamental misunderstanding. I am using the redux-observable library in React which glues redux together with RxJS for handling asynchrony. My problem is that I have to handle a large upload and I want to show progress as the file is loaded.
The function uploadFileEpic needs to return an Observable<Action> to work with redux-observable. The uploadObservable represents the workflow that I want to accomplish. If I just return the uploadObservable the upload works but I don't get any handleUploadFileProgress actions from the progressSubscriber in the ajax call. Ideally the progressSubscriber would be adding elements to another observable that I could merge with uploadObservable. You see me trying to use merge here but the TypeScript compiler complains saying the return is not assignable to an ObservableInput.
I keep going in circles so I feel my understanding must be fundamentally off. I feel like I'm missing some simple RxJS magic here. Thanks for the help!
import { Observable, Observer, Subscriber, Subject, of } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { ofType } from 'redux-observable';
import { catchError, delay, map, mergeMap, tap, merge } from 'rxjs/operators';
import { apis } from '../../config';
export const enum ActionType {
InitialFileUpload
FileProgress
UploadFileSuccess
UploadFileFail
}
const handleInitialFileUpload = (file: File, timeLimit: number) => ({
type: ActionType.InitialFileUpload,
file,
timeLimit
})
const handleFileProgress = (file: File, percentComplete: number) => ({
type: ActionType.FileProgress,
file,
percentComplete
})
const handleUploadFileSuccess = (file: File, timeLimit: number) => ({
type: ActionType.UploadFileSuccess,
file,
timeLimit
})
const handleUploadFileFail = (file: File, timeLimit: number) => ({
type: ActionType.UploadFileFail,
file,
timeLimit
})
export const uploadFileEpic= action$ =>
action$.pipe(
ofType(ActionType.InitialFileUpload),
mergeMap((action: any) => {
const { file, timeLimit } = action;
const data = new FormData()
data.append('importFile', file, file.name)
data.append('timeLimit', timeLimit)
const progressSubject = new Subject();
const ajaxRequest = {
url: apis.gateway.run,
method: 'POST',
body: data,
headers: {},
progressSubscriber: Subscriber.create(
(e: ProgressEvent) => {
const percentComplete = Math.round((e.loaded / e.total) * 100)
console.log("Progress event")
progressSubject.next(handleUploadFileProgress(file, percentComplete))
}
)
}
const uploadObservable = ajax(ajaxRequest)
.pipe(
map(res => handleUploadFileSuccess(file)),
delay(SUCCESSFUL_UPLOAD_NOTIFICATION_LENGTH),
map(() => handleUploadFileRemove(file)),
catchError(error => of(handleUploadFileFail(file, error.response)))
)
return merge(uploadObservable, progressSubject)
}
)
)
You seem to be importing merge from rxjs/operators. There, merge is treated as an operator and thus returning an OperatorFunction. By importing from simply rxjs you get the static merge that correctly returns an Observable which will be flattened by your mergeMap.

The implementation of Redux's applyMiddleware

My question is why middlewareAPI can't use :
const middlewareAPI = {
getState: store.getState,
dispatch: dispatch
}
to replace the definition in the source code as below:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args) // why not just use `dispatch: dispatch`
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
Anyone can tell me the difference ? Thanks.
It's a somewhat complicated combination of JS variable scoping/hosting, and needing to ensure that the passed-in dispatch method actually points back to the start of the middleware chain.
Please see the newly-added (and not yet published) Redux FAQ entry on why applyMiddleware uses a closure for more details.

What is the second argument for?

I find this code in a tutorial
...
import configureMockStore from 'redux-mock-store';
const middleware = [thunk];
const mockStore = configureMockStore(middleware);
...
it('should create BEGIN_AJAX_CALL & LOAD_COURSES_SUCCESS', (done) => {
const expectedActions = [
{type: types.BEGIN_AJAX_CALL},
{type: types.LOAD_COURSES_SUCCESS, body: {
courses: [{id:'clean-code', title:'Clean Code'}]
}}
];
const store = mockStore({courses:[]}, expectedActions);
store
.dispatch(courseActions.loadCourses())
.then(() => {
const actions = store.getActions();
expect(actions[0].type).toEqual(types.BEGIN_AJAX_CALL);
expect(actions[1].type).toEqual(types.LOAD_COURSES_SUCCESS);
done();
});
});
and the whole bit with expectedActions doesn't make sense.
The docs say that if there is a second argument to store, it should be a function; (no explanation telling what that function would do though).
At first I thought it was forcing some actions into the store for some reason, but a quick console.log told me that wasn't the case.
Because only dispatch causes actions to accumulate.
So is it a mistake in the text or some wisdom to explore further?
This feature was removed in version 1, but you can find the example in the pre 1 docs.
The parameter expectedActions is used for testing. You can create a mock store with an array of actions, and then dispatch an the 1st action. This action will cause the other other actions to forwarded (dispatch / next) via thunks/api middleware/etc... The test will check if all of the actions in the expectedActions array have acted on the store:
import configureStore from 'redux-mock-store';
const middlewares = []; // add your middlewares like `redux-thunk`
const mockStore = configureStore(middlewares);
// Test in mocha
it('should dispatch action', (done) => {
const getState = {}; // initial state of the store
const action = { type: 'ADD_TODO' };
const expectedActions = [action];
const store = mockStore(getState, expectedActions, done);
store.dispatch(action);
})

Is this a redux middleware anti-pattern? How to properly build async actions with middleware

Just built my first API Middleware and was just wondering where I'm suppose to chain promises for action creators that dispatch multiple actions. Is what I did an anti-pattern:
export const fetchChuck = () => {
return {
[CALL_API]: {
types: [ CHUCK_REQUEST, CHUCK_SUCCESS, CHUCK_FAILURE ],
endpoint: `jokes/random`
}
}
}
export const saveJoke = (joke) => {
return { type: SAVE_JOKE, joke: joke }
}
export const fetchAndSaveJoke = () => {
return dispatch => {
dispatch(fetchChuck()).then((response) => {
dispatch(saveJoke(response.response.value.joke))
})
}
}
Should fetchAndSaveJoke dispatch the section action in my react component or is it okay to have it as its own action creator?
I would say that at this point in the Redux world, it's not super clear what's best practice and what the anti-patterns are. It's a very unopinionated tool. While that's been great for a diverse ecosystem to flourish, it does present challenges for people looking for ways to organize their apps without running into pitfalls or excessive boilerplate. From what I can tell, your approach seems to be roughly in line with the advice from the Redux guide. The one thing that looks funny to me is that it seems like CHUCK_SUCCESS should probably make SAVE_JOKE unnecessary.
I personally find it rather awkward to have action creators dispatch more actions, and so I worked out the approach behind react-redux-controller. It's brand new, so it's certainly not a "best practice", but I'll throw it out there in case you or someone else wants to give it a try. In that workflow, you'd have a controller method that looks something like:
// actions/index.js
export const CHUCK_REQUEST = 'CHUCK_REQUEST';
export const CHUCK_SUCCESS = 'CHUCK_SUCCESS';
export const CHUCK_FAILURE = 'CHUCK_FAILURE';
export const chuckRequest = () => { type: CHUCK_REQUEST };
export const chuckSuccess = (joke) => { type: CHUCK_SUCCESS, joke };
export const chuckFailure = (err) => { type: CHUCK_FAILURE, err };
// controllers/index.js
import fetch from 'isomorphic-fetch'; // or whatever
import * as actions from '../actions';
const controllerGenerators = {
// ... other controller methods
*fetchAndSaveJoke() {
const { dispatch } = yield getProps;
// Trigger a reducer to set a loading state in your store, which the UI can key off of
dispatch(actions.chuckRequest());
try {
const response = yield fetch('jokes/random');
dispatch(actions.chuckSuccess(response.response.value.joke));
} catch(err) {
dispatch(actions.chuckFailure(err));
}
},
};

Resources