Using dispatch in useffect hook of functional component,
Below code shows error page like below;
Component:
import { GetParks } from "../../../redux/actions/survey_actions"
...
function BarcodeGenerator(props) {
const dispatch = useDispatch();
useEffect(() => {
dispatch(props.GetParks());
}, []);
actions:
export const GetParks = (Id) => async (dispatch, getState) => {
try {
const response = await axiosHelper.get("api/survey/GetParks", {
params: {
Id,
},
});
debugger;
response = response.data;
if (response.status !== ResponseStatus.SUCCESS) {
dispatch({
type: GET_PARKS,
payload: [1, 4555, 34],
});
}
} catch (error) {
catchCallback(error);
}
};
const _getParks = (data) => ({
type: GET_PARKS,
payload: data,
});
how does dispatch the action to reducer properly
Action must be a plain object, as it is described in the error description. E.g. it is ok to use dispatch directly as is:
if (*statement*) {
dispatch({
action: DO_SMTH,
payload: true
})
}
or to make the action creator returning the equal object if you want to make clean re-usable code:
if (*statement*) {
dispatch(doSmth(true));
}
function doSmth(toggle) {
return ({
action: DO_SMTH,
payload: toggle
})
}
Related
Packages:
redux-observable#2.0.0-rc.2
rxjs latest
universal-rxjs-ajax dev branch
next-redux-wrapper latest
next.js latest
I have a simple Page with getStaticProps:
export const getStaticProps = wrapper.getStaticProps((store) => async (ctx) => {
store.dispatch({ type: 'ADD_DATA' });
// const response = await fetch('https://rickandmortyapi.com/api');
// const data = await response.json();
// store.dispatch({ type: 'SERVER_ACTION', payload: data.characters });
return {
props: {},
};
});
Action 'ADD_DATA' triggers action 'SERVER_ACTION':
export const AddDataEpic: Epic = (action$) =>
action$.pipe(
ofType('ADD_DATA'),
mergeMap((action) =>
request({ url: 'https://rickandmortyapi.com/api' }).pipe(
map((response) => {
return {
type: 'SERVER_ACTION',
payload: response.response.characters,
};
})
)
)
);
Inside the reducer in the case 'SERVER_ACTION': clause I receive the payload:
const server = (state: State = { data: null }, action: AnyAction) => {
switch (action.type) {
case HYDRATE: {
console.log('HYDRATE >', action.payload); // logs out "HYDRATE > { server: { data: null } }"
return {
...state,
...state.server,
...action.payload.server,
};
}
case 'SERVER_ACTION': {
console.log('SERVER_ACTION >', action.payload); // logs out "SERVER_ACTION > https://rickandmortyapi.com/api/character"
return {
...state,
...state.server,
data: action.payload,
};
}
default:
return state;
}
};
But the payload isn't passed to HYDRATE action:
console.log('HYDRATE >', action.payload); // logs out "HYDRATE > { server: { data: null } }"
If I dispatch the 'SERVER_ACTION' action from inside the getStaticProps:
export const getStaticProps = wrapper.getStaticProps((store) => async (ctx) => {
// store.dispatch({ type: 'ADD_DATA' });
const response = await fetch('https://rickandmortyapi.com/api');
const data = await response.json();
store.dispatch({ type: 'SERVER_ACTION', payload: data.characters });
return {
props: {},
};
});
The HYDRATE action inside the reducer receive the payload:
HYDRATE > { server: { data: 'https://rickandmortyapi.com/api/character' } }
I don't understand what's wrong with my code.
May it be a bug in one of the libraries? Or is it a mistake in my code?
If anyone has any suggestions, PLEASE
#PYTHON DEVELOPER999 It might be due to the latest update on next-redux-wrapper, there are few migration steps =>
https://github.com/kirill-konshin/next-redux-wrapper#upgrade-from-6x-to-7x
I'm trying to call apiAction in constructor method through the dispatch redux method in ReactJS Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './styles.scss'
import { fetchData, testSet } from '../../../../../event/src/cg-store/actions';
class AppDetails extends Component {
constructor(props) {
super(props);
this.state ={
testowaZmienna: ''
}
this.props.fetchData('5576900');
}
componentDidMount() {
document.addEventListener('click', () => {
this.props.addCount()
});
this.props.testSet()
this.props.fetchData('5576900');
console.log('dhsadhnaskjndaslndsadl-----------------------------------------')
}
render() {
const { error, test, count, testSetData, data } = this.props;
return (
<div>
TEST--------------------------
Count: {count}
Error: {error}
Test: {test}
TestSet: {testSetData}
Fetch: {data[0]}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
count: state.count,
test: state.cg.test,
data: state.cg.data,
error: state.cg.error,
testSetData: state.cg.testSet,
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (offerId) => dispatch(fetchData(offerId)),
addCount: () => dispatch({
type: 'ADD_COUNT'
}),
testSet: () => dispatch(testSet()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AppDetails);
As you can see there is addCount, testSet and fetchData methods. addCount and testSet works but problem is with fetchData:
This is apiAction method:
const fetchProductsPending = () => {
return {
type: actionTypes.FETCH_DATA_PENDING
};
};
const fetchProductsSuccess = fetchedData => {
return {
type: actionTypes.FETCH_DATA_SUCCESS,
data: fetchedData
};
};
const fetchProductsError = errorMessage => {
return {
type: actionTypes.FETCH_DATA_ERROR,
error: errorMessage
};
};
export const testSet = () => {
return {
type: actionTypes.TEST_SET
};
};
export const fetchData = (offerId) => (dispatch) => {
console.log('Im inside fetch before set pending'); // It does not want to go here
dispatch(fetchProductsPending());
axios
.get(config.api.host + offerId, {
headers: {
"Content-Type": "application/json",
}
})
.then(response => {
return response.data;
})
.then(response => {
dispatch(fetchProductsSuccess(response.data));
console.log("Fetch data success: ----------------------");
console.log(response.data);
})
.catch(error => {
dispatch(fetchProductsError(error.statusText));
console.log("Fetch data success: ----------------------");
console.log(error);
});
};
So as you can see testSet works fine but fetchData does not want to work.
What I'm doing wrong?
My specs are behaving weirdly in that when I run the tests alone, they pass. However, when I run the test suite all together, the failure tests still continue to use the success axios mock instead of using the correct failing http axios mock. This results in my tests failing. Am I missing something for isolating the 2 mocks from each other in the different portions of code?
jobactions.js
export const loadUnassignedJobs = (job_type) => {
if (!['unscheduled', 'overdue'].includes(job_type)) {
throw 'Job Type must be "unscheduled" or "overdue".';
}
return (dispatch) => {
dispatch({type: JobActionTypes.LOAD_UNASSIGNED_JOBS_STARTED, job_type });
return axios.get(defaults.baseapi_uri + 'jobs/' + job_type)
.then(function (response) {
dispatch(updateUnassignedJobs(response.data.jobs));
// handle success
})
.catch(function (error) {
// handle error
dispatch({ type: JobActionTypes.LOAD_UNASSIGNED_JOBS_FAILURE, error });
})
.then(function () {
// always executed
});
}
};
export const updateUnassignedJobs = (unassigned_jobs) => {
let unassigned_job_ids = [];
let jobs = {};
for (let job of unassigned_jobs) {
unassigned_job_ids.push(job.id);
jobs[job.id]=job;
}
return({
type: JobActionTypes.LOAD_UNASSIGNED_JOBS_SUCCESS,
jobs,
unassigned_job_ids,
});
};
spec.js
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import * as jobActions from "../../../app/javascript/actions/JobActions"
import { findAction } from '../support/redux_store'
import * as JobActionTypes from '../../../app/javascript/constants/JobActionTypes'
import fixtures_jobs_unscheduled_success from '../fixtures/jobs_unscheduled_success'
import moxios from "moxios";
export const mockStore = configureMockStore([thunk]);
let store;
describe ('loadUnassignedJobs', () => {
context('when bad parameters are passed', async () => {
it('will raise an error', () => {
const store = mockStore();
expect(() => {
store.dispatch(jobActions.loadUnassignedJobs('wrong_type'));
}).to.throw('Job Type must be "unscheduled" or "overdue".');
});
});
context('when unscheduled is passed', () => {
beforeEach(() => {
moxios.install();
console.log("before each called");
console.log(moxios.requests);
store = mockStore();
store.clearActions();
});
afterEach(() => {
console.log("after each called");
console.log(moxios.requests);
moxios.uninstall();
});
context('on success', () => {
beforeEach(() => {
moxios.wait(() => {
let request = moxios.requests.mostRecent();
request.respondWith({
status: 200,
response: fixtures_jobs_unscheduled_success
});
});
})
it('dispatches LOAD_UNASSIGNED_JOBS_STARTED', () => {
store.dispatch(jobActions.loadUnassignedJobs('unscheduled')).then(() => {
expect(findAction(store, JobActionTypes.LOAD_UNASSIGNED_JOBS_STARTED)).to.be.eql({
type: JobActionTypes.LOAD_UNASSIGNED_JOBS_STARTED,
job_type: 'unscheduled'
});
});
});
it('dispatches updateUnassignedJobs()', () => {
store.dispatch(jobActions.loadUnassignedJobs('unscheduled')).then(() => {
expect(findAction(store,JobActionTypes.LOAD_UNASSIGNED_JOBS_SUCCESS)).to.be.eql(jobActions.updateUnassignedJobs(fixtures_jobs_unscheduled_success.jobs))
});
});
});
context('on error', () => {
beforeEach(() => {
//console.log("before each on error called");
//console.log(moxios.requests);
moxios.wait(() => {
console.log('after waiting for moxios..')
console.log(moxios.requests);
let request = moxios.requests.mostRecent();
request.respondWith({
status: 500,
response: { error: 'internal server error' }
});
});
})
it('dispatches LOAD_UNASSIGNED_JOBS_FAILURE', (done) => {
console.log(moxios.requests);
store.dispatch(jobActions.loadUnassignedJobs('unscheduled')).then(() => {
console.log(moxios.requests);
console.log(store.getActions());
expect(findAction(store, JobActionTypes.LOAD_UNASSIGNED_JOBS_FAILURE)).to.include({
type: JobActionTypes.LOAD_UNASSIGNED_JOBS_FAILURE
});
expect(findAction(store, JobActionTypes.LOAD_UNASSIGNED_JOBS_FAILURE).error).to.include({
message: 'Request failed with status code 500'
});
done();
});
});
it('does not dispatch LOAD_UNASSIGNED_JOBS_SUCCESS', (done) => {
store.dispatch(jobActions.loadUnassignedJobs('unscheduled')).then(() => {
expect(findAction(store, JobActionTypes.LOAD_UNASSIGNED_JOBS_SUCCESS)).to.be.undefined;
done();
});
});
})
});
});
describe('updateUnassignedJobs', () => {
it('assigns jobs to hash and creates an unassigned_job_ids array', () => {
expect(jobActions.updateUnassignedJobs([ { id: 1, step_status: 'all_complete' }, { id: 2, step_status: 'not_started' } ])).to.be.eql(
{
type: JobActionTypes.LOAD_UNASSIGNED_JOBS_SUCCESS,
jobs: { 1: { id: 1, step_status: 'all_complete' }, 2: { id: 2, step_status: 'not_started' } },
unassigned_job_ids: [ 1,2 ]
}
)
});
});
Found the issue!
The it() blocks for the success case were not using the done callback causing the afterEach() moxios.uninstall() to be called prematurely and not resetting the requests after the call was complete. Fixing this, and now all the tests pass.
I have been playing with rxjs and redux-observable for the last few days and have been struggle to find a way to a test for Observable.ajax. I have the following epic which create a request to https://jsonplaceholder.typicode.com/,
export function testApiEpic (action$) {
return action$.ofType(REQUEST)
.switchMap(action =>
Observable.ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
)
}
where,
export const REQUEST = 'my-app/testApi/REQUEST'
export const SUCCESS = 'my-app/testApi/SUCCESS'
export const FAILURE = 'my-app/testApi/FAILURE'
export const CLEAR = 'my-app/testApi/CLEAR'
export function requestTestApi () {
return { type: REQUEST }
}
export function successTestApi (response) {
return { type: SUCCESS, response }
}
export function failureTestApi (error) {
return { type: FAILURE, error }
}
export function clearTestApi () {
return { type: CLEAR }
}
The code works fine when runs in browser but not when testing with Jest.
I have try,
1) Create a test based on https://redux-observable.js.org/docs/recipes/WritingTests.html. The store.getActions() returns only { type: REQUEST }.
const epicMiddleware = createEpicMiddleware(testApiEpic)
const mockStore = configureMockStore([epicMiddleware])
describe.only('fetchUserEpic', () => {
let store
beforeEach(() => {
store = mockStore()
})
afterEach(() => {
epicMiddleware.replaceEpic(testApiEpic)
})
it('returns a response, () => {
store.dispatch({ type: REQUEST })
expect(store.getActions()).toEqual([
{ type: REQUEST },
{ type: SUCCESS, response }
])
})
})
2) Create a test based on Redux-observable: failed jest test for epic. It returns with
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
it('returns a response', (done) => {
const action$ = ActionsObservable.of({ type: REQUEST })
const store = { getState: () => {} }
testApiEpic(action$, store)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
{ type: SUCCESS, response }
])
done()
})
})
Can someone point me out what is the correct way to test Observable.ajax ?
I would follow the second example, from StackOverflow. To make it work you'll need to make some minor adjustments. Instead of importing Observable.ajax in your epic file and using that reference directly, you need to use some form of dependency injection. One way is to provide it to the middleware when you create it.
import { ajax } from 'rxjs/observable/dom/ajax';
const epicMiddleware = createEpicMiddleware(rootEpic, {
dependencies: { ajax }
});
The object we passed as dependencies will be give to all epics as the third argument
export function testApiEpic (action$, store, { ajax }) {
return action$.ofType(REQUEST)
.switchMap(action =>
ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
);
}
Alternatively, you could not use the dependencies option of the middleware and instead just use default parameters:
export function testApiEpic (action$, store, ajax = Observable.ajax) {
return action$.ofType(REQUEST)
.switchMap(action =>
ajax({ url, method })
.map(data => successTestApi(data.response))
.catch(error => failureTestApi(error))
.takeUntil(action$.ofType(CLEAR))
);
}
Either one you choose, when we test the epic we can now call it directly and provide our own mock for it. Here are examples for success/error/cancel paths These are untested and might have issues, but should give you the general idea
it('handles success path', (done) => {
const action$ = ActionsObservable.of(requestTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.of({ url, method })
};
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
successTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
])
done();
});
});
it('handles error path', (done) => {
const action$ = ActionsObservable.of(requestTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.throw({ url, method })
};
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe(actions => {
expect(actions).to.deep.equal([
failureTestApi({ url: '/whatever-it-is', method: 'WHATEVERITIS' })
])
done();
});
});
it('supports cancellation', (done) => {
const action$ = ActionsObservable.of(requestTestApi(), clearTestApi())
const store = null; // not used by epic
const dependencies = {
ajax: (url, method) => Observable.of({ url, method }).delay(100)
};
const onNext = chai.spy();
testApiEpic(action$, store, dependencies)
.toArray()
.subscribe({
next: onNext,
complete: () => {
onNext.should.not.have.been.called();
done();
}
});
});
For the first way:
First, use isomorphic-fetch instead of Observable.ajax for nock support, like this
const fetchSomeData = (api: string, params: FetchDataParams) => {
const request = fetch(`${api}?${stringify(params)}`)
.then(res => res.json());
return Observable.from(request);
};
So my epic is:
const fetchDataEpic: Epic<GateAction, ImGateState> = action$ =>
action$
.ofType(FETCH_MODEL)
.mergeMap((action: FetchModel) =>
fetchDynamicData(action.url, action.params)
.map((payload: FetchedData) => fetchModelSucc(payload.data))
.catch(error => Observable.of(
fetchModelFail(error)
)));
Then, you may need an interval to decide when to finish the test.
describe("epics", () => {
let store: MockStore<{}>;
beforeEach(() => {
store = mockStore();
});
afterEach(() => {
nock.cleanAll();
epicMiddleware.replaceEpic(epic);
});
it("fetch data model succ", () => {
const payload = {
code: 0,
data: someData,
header: {},
msg: "ok"
};
const params = {
data1: 100,
data2: "4"
};
const mock = nock("https://test.com")
.get("/test")
.query(params)
.reply(200, payload);
const go = new Promise((resolve) => {
store.dispatch({
type: FETCH_MODEL,
url: "https://test.com/test",
params
});
let interval: number;
interval = window.setInterval(() => {
if (mock.isDone()) {
clearInterval(interval);
resolve(store.getActions());
}
}, 20);
});
return expect(go).resolves.toEqual([
{
type: FETCH_MODEL,
url: "https://test.com/assignment",
params
},
{
type: FETCH_MODEL_SUCC,
data: somData
}
]);
});
});
enjoy it :)
I have the following middleware that I use to call similar async calls:
import { callApi } from '../utils/Api';
import generateUUID from '../utils/UUID';
import { assign } from 'lodash';
export const CALL_API = Symbol('Call API');
export default store => next => action => {
const callAsync = action[CALL_API];
if(typeof callAsync === 'undefined') {
return next(action);
}
const { endpoint, types, data, authentication, method, authenticated } = callAsync;
if (!types.REQUEST || !types.SUCCESS || !types.FAILURE) {
throw new Error('types must be an object with REQUEST, SUCCESS and FAILURE');
}
function actionWith(data) {
const finalAction = assign({}, action, data);
delete finalAction[CALL_API];
return finalAction;
}
next(actionWith({ type: types.REQUEST }));
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
I am then making the following calls in componentWillMount of a component:
componentWillMount() {
this.props.fetchResults();
this.props.fetchTeams();
}
fetchTeams for example will dispatch an action that is handled by the middleware, that looks like this:
export function fetchTeams() {
return (dispatch, getState) => {
return dispatch({
type: 'CALL_API',
[CALL_API]: {
types: TEAMS,
endpoint: '/admin/teams',
method: 'GET',
authenticated: true
}
});
};
}
Both the success actions are dispatched and the new state is returned from the reducer. Both reducers look the same and below is the Teams reducer:
export const initialState = Map({
isFetching: false,
teams: List()
});
export default createReducer(initialState, {
[ActionTypes.TEAMS.REQUEST]: (state, action) => {
return state.merge({isFetching: true});
},
[ActionTypes.TEAMS.SUCCESS]: (state, action) => {
return state.merge({
isFetching: false,
teams: action.payload.response
});
},
[ActionTypes.TEAMS.FAILURE]: (state, action) => {
return state.merge({isFetching: false});
}
});
The component then renders another component that dispatches another action:
render() {
<div>
<Autocomplete items={teams}/>
</div>
}
Autocomplete then dispatches an action in its componentWillMount:
class Autocomplete extends Component{
componentWillMount() {
this.props.dispatch(actions.init({ props: this.exportProps() }));
}
if an error happens in the autocomplete reducer that is invoked after the SUCCESS reducers have been invoked for fetchTeams and fetchResults from the original calls in componentWillMount of the parent component and the error will be handled in the Promise.catch of the callApi method that happens in the middleware.
return callApi(endpoint, method, data, authenticated).then(response => {
return next(actionWith({
type: types.SUCCESS,
payload: {
response
}
}))
}).catch(error => {
return next(actionWith({
type: types.FAILURE,
error: true,
payload: {
error: error,
id: generateUUID()
}
}))
});
};
This is because it is happening with in the same tick of the event loop. If I introduce some asynchronicity in the Autcomplete componentWIllMount function then the error is not handled in the Promise catch handler of the middleware
class Autocomplete extends Component{
componentWillMount() {
setTimeout(() => {
this.props.dispatch(actions.init({ props: this.exportProps() }));
});
}
Should I have the callApi function execute on a separate event loop tick?