NextJS: `HYDRATION` action doesn't receive server payload when using `redux-observable` - rxjs

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

Related

Redux and data fetching

I am a newbie to React and Redux hope the folks here can help me. I am trying to make 2 api calls as shown below. However only the first api call seem to get run so my rootCategories end up always being set to null. How can I ensure second api call also get executed before state being changed?
Reducer
export const categoryListReducer = (state = {categories: [], rootCategories: []}, action) => {
switch (action.type) {
case CATEGORY_LIST_REQUEST:
return {loading: true, categories: [], rootCategories: []}
case ROOT_CATEGORY_LIST_REQUEST:
return {loading: true, rootCategories: []}
case CATEGORY_LIST_SUCCESS:
return {...state, loading: false, categories: action.payload[0], rootCategories: action.payload[1]}
case ROOT_CATEGORY_LIST_SUCCESS:
return {...state, loading: false, rootCategories: action.payload}
case ROOT_CATEGORY_LIST_FAIL:
return {loading: false, error: action.payload}
case CATEGORY_LIST_FAIL:
return {loading: false, error: action.payload}
default:
return state
}
}
Action
export const listCategories = (breadcrumbs) => async(dispatch) => {
try {
//fire off first reducer and load off empty array of products
dispatch({
type: CATEGORY_LIST_REQUEST
})
const apiEndPoint = breadcrumbs ?
`/api/products/categories/${breadcrumbs}/` :
'/api/products/categories/'
const {
data
} = await axios(apiEndPoint)
const {
data2
} = await axios('/api/products/categories/')
dispatch({
type: CATEGORY_LIST_SUCCESS,
payload: [data, data2],
})
} catch (error) {
console.log('error ' + error)
dispatch({
type: CATEGORY_LIST_FAIL,
payload: error.response && error.response.data.message ? error.response.data.message : error.response.data
})
}
}
Try this:
if (data & data2) {
dispatch({
type: CATEGORY_LIST_SUCCESS,
payload: [data, data2]
})
}
Now, the dispatch is only executed if both const are set.
EDIT:
I found this post about how Axios seems to have its own way to fetch two URLs at once, like this:
import axios from 'axios';
let one = "https://api.storyblok.com/v1/cdn/stories/health?version=published&token=wANpEQEsMYGOwLxwXQ76Ggtt"
let two = "https://api.storyblok.com/v1/cdn/datasources/?token=wANpEQEsMYGOwLxwXQ76Ggtt"
const requestOne = axios.get(one);
const requestTwo = axios.get(two);
axios.all([requestOne, requestTwo]).then(axios.spread((...responses) => {
const responseOne = responses[0]
const responseTwo = responses[1]
// use/access the results
})).catch(errors => {
// react on errors.
})

React - can't dispatch action in hook component

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
})
}

Nest.js handling errors for HttpService

I'm trying to test NestJS's built in HttpService (which is based on Axios). I'm having trouble testing error/exception states though. In my test suite I have:
let client: SomeClearingFirmClient;
const mockConfigService = {
get: jest.fn((type) => {
switch(type) {
case 'someApiBaseUrl': {
return 'http://example.com'
}
case 'someAddAccountEndpoint': {
return '/ClientAccounts/Add';
}
case 'someApiKey': {
return 'some-api-key';
}
default:
return 'test';
}
}),
};
const successfulAdd: AxiosResponse = {
data: {
batchNo: '39cba402-bfa9-424c-b265-1c98204df7ea',
warning: '',
},
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
const failAddAuth: AxiosError = {
code: '401',
config: {},
name: '',
message: 'Not Authorized',
}
const mockHttpService = {
post: jest.fn(),
get: jest.fn(),
}
it('Handles a failure', async () => {
expect.assertions(1);
mockHttpService.post = jest.fn(() => of(failAddAuth));
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: HttpService,
useValue: mockHttpService,
},
SomeClearingFirmClient,
],
}).compile();
client = module.get<SomeClearingFirmClient>(SomeClearingFirmClient);
const payload = new SomeClearingPayload();
try {
await client.addAccount(payload);
} catch(e) {
console.log('e', e);
}
});
And my implementation is:
async addAccount(payload: any): Promise<SomeAddResponse> {
const addAccountEndpoint = this.configService.get('api.someAddAccountEndpoint');
const url = `${this.baseUrl}${addAccountEndpoint}?apiKey=${this.apiKey}`;
const config = {
headers: {
'Content-Type': 'application/json',
}
};
const response = this.httpService.post(url, payload, config)
.pipe(
map(res => {
return res.data;
}),
catchError(e => {
throw new HttpException(e.response.data, e.response.status);
}),
).toPromise().catch(e => {
throw new HttpException(e.message, e.code);
});
return response;
}
Regardless of whether I use Observables or Promises, I can't get anything to catch. 4xx level errors sail on through as a success. I feel like I remember Axios adding some sort of config option to reject/send an Observable error to subscribers on failures... but I could be imagining that. Am I doing something wrong in my test harness? The other StackOverflow posts I've seen seem to say that piping through catchError should do the trick, but my errors are going through the map operator.
Your mockHttpService seems to return no error, but a value:
mockHttpService.post = jest.fn(() => of(failAddAuth));
What of(failAddAuth) does is to emit a value(failAddAuth) and then complete.
That's why the catchError from this.httpService.post(url, payload, config) will never be reached, because no errors occur.
In order to make sure that catchError is hit, the observable returned by post() must emit an error notification.
You could try this:
// Something to comply with `HttpException`'s arguments
const err = { response: 'resp', status: '4xx' };
mockHttpService.post = jest.fn(() => throwError(err));
throwError(err) is the same as new Observable(s => s.error(err))(Source code).

How to write an action that updates Redux's store?

I have a web app where use React-Redux. There is React table (list) that I need to populate with data from database. I use WebApi on the server and automatically generated (by TypeWriter) web-api on the client. The key parts of code looks as following:
1) Routing:
<Route path="/Dictionary/:dictionaryName" component={Dictionary} />
2) State:
export type SingleDictionaryState = Readonly<{
singleDictionary: WebApi.SingleDictionary[];
}>;
export const initialState: SingleDictionaryState = {
singleDictionary: [],
};
3) Reducer:
export const reducer: Reducer<SingleDictionaryState> = (state: SingleDictionaryState = initialState, action: AllActions): SingleDictionaryState => {
switch (action.type) {
case getType(actions.setSingleDictionaryValue):
return { ...state, ...action.payload };
}
return state;
};
4) Actions:
const actionsBasic = {
setSingleDictionaryValue: createAction('singleDictionary/setSingleDictionaryValue', (singleDictionary: any) => singleDictionary),
};
const actionsAsync = {
getDictionaryByName: (dictionaryName: string) => {
const currentState = store.getState().singleDictionary;
WebApi.api.dictionaryQuery.getDictionary(capitalizeForApi(dictionaryName));
},
};
export const actions = Object.assign(actionsBasic, actionsAsync);
const returnsOfActions = Object.values(actionsBasic).map($call);
export type AllActions = typeof returnsOfActions[number];
5) Container:
const mapStateToProps = (state: AppState, ownProps: OwnProps): StateProps => ({
dictionaryType: state.singleDictionary,
});
const mapDispatchToProps = (dispatch: Dispatch<any>): DispatchProps => ({
onLoad: (dictionaryName: string) => {
Actions.singleDictionary.getDictionaryByName(dictionaryName);
},
});
export default withRouter(connect<StateProps, DispatchProps, OwnProps>(mapStateToProps, mapDispatchToProps)(DictionaryPage));
6) The client web-api:
class DictionaryQueryService {
getDictionary(name: string) {
const user = store.getState().oidc.user;
const headers = new Headers();
headers.append('Accept', 'application/json');
headers.append('Content-Type', 'application/json');
headers.append('Cache-Control', 'no-cache, no-store, must-revalidate');
headers.append('Pragma', 'no-cache');
headers.append('Expires', '0');
if (user) {
headers.append('Authorization', `Bearer ${user.access_token}`);
}
return () => {
return fetch(`api/dictionaries/${encodeURIComponent(name)}`, {
method: 'get',
headers,
})
.then(response => {
if (!response.ok) {
const traceId = response.headers.get("X-Trace-Id");
throw new ApiError(`${response.status} ${response.statusText}`, traceId);
}
return response.status == 204 ? null : response.json() as Promise<any[]>;
});
};
}
Actually, I'm not sure how to write my getDictionaryByName action.
Just my 2 cents. I use ES6 syntax, but Typescript would work as similar way.
actionTypes.js
export const RESET_DICTIONARIES = 'RESET_DICTIONARIES';
export const LOAD_DICTIONARIES_REQUEST = 'LOAD_DICTIONARIES_REQUEST';
export const LOAD_DICTIONARIES_REQUEST_SUCCESS = 'LOAD_DICTIONARIES_REQUEST_SUCCESS';
export const LOAD_DICTIONARIES_REQUEST_FAILURE = 'LOAD_DICTIONARIES_REQUEST_FAILURE';
dictionaryActions.js
/* Load Dictionariies*/
export function loadDictionariesRequestBegin() {
return {type: types.LOAD_DICTIONARIES_REQUEST};
}
export function loadDictionariesRequest(name) {
return function(dispatch) {
dispatch(loadDictionariesRequestBegin());
// eslint-disable-next-line no-undef
const request = new Request(`${YOUR_URL}/api/dictionaries/{name}`, {
method: 'get',
headers: new Headers({
'Content-type': 'application/json',
'Accept': 'application/json',
'Authorization': auth.getToken(),
})
});
return fetch(request)
.then(
response => {
if (!response.ok) {
dispatch(loadDictionariesRequestFailure(response.statusText));
throw new Error(response.statusText);
}
return response.json();
},
error => {
dispatch(loadDictionariesRequestFailure(error));
throw error;
})
.then(dictionaries=> {
if (dictionaries) {
dispatch(loadDictionariesRequestSuccess(dictionaries));
return dictionaries;
} else {
throw new Error('dictionaries NOT found in response');
}
});
};
}
export function loadDictionariesRequestSuccess(dictionaries) {
return {type: types.LOAD_DICTIONARIES_REQUEST_SUCCESS, dictionaries};
}
export function loadDictionariesRequestFailure(error) {
return {type: types.LOAD_DICTIONARIES_REQUEST_FAILURE, error};
}
dictionaryReducer.js
export default function dictionaryReducer(state = initialState.dictionaries, action) {
switch (action.type) {
case types.RESET_DICTIONARIES:
return {
...state,
loaded: false,
loading: false,
error: null,
};
/* load dictionaries*/
case types.LOAD_DICTIONARIES_REQUEST:
return {
...state,
error: null,
loaded: false,
loading: true
};
case types.LOAD_DICTIONARIES_REQUEST_SUCCESS:
return {
...state,
data: action.dictionaries,
error: null,
loaded: true,
loading: false
};
case types.LOAD_DICTIONARIES_REQUEST_FAILURE:
return {
...state,
loaded: false,
loading: false,
error: action.error
};
return state;
}
initialState.js
export default {
actions: {},
dictionaries: {
data: [],
loaded: false,
loading: false,
error: null,
},
}
dictionary client side API
this.props.actions
.loadDictionaryRequest(name)
.then(data => {
this.setState({ data: data, errorMessage: '' });
})
.then(() => {
this.props.actions.resetDictionaries();
})
.catch(error => {
...
});
Hope this may help.

How can I test Observable.ajax (redux-observable)?

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 :)

Resources