I am unable to dispatch two actions together in react thunk - react-native

I am fetching two APIs in parallel and I want to render the data together but I am unable to render the data from the second API.
I am attaching the expo link of the project.
https://snack.expo.dev/#keshav1973/thunk

Let minify fetchUsers function as below
const getLinksDataPromises = async ()=> {
const links = ["https://jsonplaceholder.typicode.com/users","https://jsonplaceholder.typicode.com/posts"]
return links.map( async link=> {
const response = await fetch(link)
const data = await response.json()
return data
})
}
export const fetchUsers = () => {
return async dispatch => {
dispatch(getUsersRequest());
const promises = await getLinksDataPromises()
const results = await Promise.all(promises);
const users = results[0]
const posts = results[1]
dispatch({ type: 'GET_USERS_SUCCESS', payload: users})
dispatch({ type: 'GET_POSTS_SUCCESS', payload: posts})
};
};
The issue was not copying the previous state value before you update the new value. That's why the second dispatch clear the previous dispatched value.
Updated reducer function
const initialState = {
users: [],
posts: [],
loading: false,
error: null,
};
const users = (state = initialState, action) => {
console.log({action})
switch (action.type) {
case 'GET_USERS_REQUEST':
return {...state, loading: true};
case 'GET_USERS_SUCCESS':
return {...state,loading: false, users: action.payload};
case 'GET_USERS_FAILURE':
return {...state,loading: false, error: action.payload};
case 'GET_POSTS_SUCCESS':
return {...state,loading: false, posts: action.payload,}
default:
return state;
}
};
export default users;
You can test working example here
https://snack.expo.dev/#emmbyiringiro/230e21

Related

Can't append reducer state in redux toolkit

I want to append the state.data with action.payload and i have tried everything it gives unidefined on .concat, .push or spreading in an array.
import { createAction, createReducer } from "#reduxjs/toolkit";
const initialState = {};
const request = createAction("allDataRequest");
const success = createAction("allDataSuccess");
const fail = createAction("allDataFailure");
const clear = createAction("clearErrors");
export const allServicesReducer = createReducer(initialState, (builder) => {
builder
.addCase(request, (state, action) => {
state.loading = true;
})
.addCase(success, (state, action) => {
state.loading = false;
state.data = action.payload;
// I want to append this state.data with payload
//state.data = state.data.concat(action.payload)
//state.data = [...state.data, ...action.payload]
// However i get stae.data undefined on both
})
.addCase(fail, (state, action) => {
state.loading = false;
state.error = action.payload;
})
.addCase(clear, (state, action) => {
state.error = null;
});
});
This is because your initial state does not have a .data field:
const initialState = {};
So, yes, it's going to be empty to begin with.
You need to either provide const initialState = {data: []} so there is a field, or update the reducer logic to handle the case where it doesn't exist.

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

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.

WebSocket onmessage is not triggered when onsend is called

I am developing a stateless typescript backend with WebSocket. I created a SocketMiddleware as a middleware to my redux state based on dev.io tutorial. The first socket.send() message from onopen works fine. However, I can't trigger the subsequent onmessage using SEND_MSG dispatch.
The backend shows that it receives a log but it is not received by the clients. I am sure that the connection_id is already set correctly
const socketMiddleware = () => {
let socket = null;
const onOpen = (store) => (event) => {
store.dispatch({ type: "WS_CONNECTED" });
};
const onClose = (store) => () => {
store.dispatch({ type: "WS_DISCONNECTED" });
};
const onMessage = (store) => (message) => {
console.log("message received #middleware, ", message.data);
const payload = JSON.parse(message.data);
switch (payload.action) {
case "get_connection_id":
const { connectionId } = payload;
store.dispatch({
type: "UPDATE_MY_CONNECTION_ID",
payload: { myConnectionId: connectionId },
});
break;
case "join_room_socket":
const { match_id, players, connectionIdArr } = payload;
if (match_id) {
store.dispatch({
type: "UPDATE_ROOM",
payload: {
players: players,
match_id: match_id,
connectionIdArr: connectionIdArr,
},
});
}
break;
case "broadcast_action":
const { move } = body;
store.dispatch({
type: "UPDATE_GAME_STATE",
payload: { move: move },
});
}
};
return (store) => (next) => (action) => {
switch (action.type) {
case "WS_CONNECT":
if (socket !== null) socket.close();
socket = new WebSocket(process.env.WSS_ENDPOINT);
socket.onmessage = onMessage(store);
socket.onclose = onClose(store);
socket.onopen = onOpen(store);
break;
case "WS_CONNECTED":
console.log("WebSocket client is connected");
socket.send(JSON.stringify({ action: "get_connection_id" }));
case "WS_DISCONNECTED":
console.log("WebSocket client is disconnected");
case "SEND_MSG":
console.log("sending a message", action);
socket.send(JSON.stringify({ ...action.payload }));
default:
console.log("the next action:", action);
return next(action);
}
};
};
export default socketMiddleware();
my redux store
...
const persistConfig = {
key: "root",
storage: AsyncStorage,
};
const persistedReducer = persistReducer(persistConfig, reducer);
const store = createStore(
persistedReducer,
compose(applyMiddleware(reduxThunk, wsMiddleware))
);
my backend side:
joinRoomSocket: (data) => {
const body = JSON.parse(data.body);
body.connectionIdArr.map((connectionId) => {
const endpoint = `${data.requestContext.domainName}/${data.requestContext.stage}`;
const apigwManagementApi = new AWS.ApiGatewayManagementApi({
apiVersion: "2018-11-29",
endpoint,
});
const params = {
ConnectionId: connectionId,
Data: JSON.stringify(body),
};
return apigwManagementApi.postToConnection(params).promise();
});
},
Which onmessage are you referring to server or client? If you are referring to onMessage callback of client. For this you need to send something from the server using websocket.send('text message').
For client to receive message on onmessage event, server needs to send data.
Here is the flow:
Client ws.onsend('abc') ----------------> Server ws.onmessage(data)
Server ws.onsend('abc') ----------------> Client ws.onmessage(data)```

how to use redux-observerable when the network requset handle by axios

I want to use redux-observerable to my project,because the action of if can be canceld.But the offical gives the example which uses the ajax of rxjs, I want to use axios as the network library, how to realize it.
the example code:
const FETCH_USER = 'FETCH_USER';
const FETCH_USER_FULFILLED = 'FETCH_USER_FULFILLED';
const FETCH_USER_REJECTED = 'FETCH_USER_REJECTED';
const FETCH_USER_CANCELLED = 'FETCH_USER_CANCELLED';
const fetchUser = id => ({ type: FETCH_USER, payload: id });
const fetchUserFulfilled = payload => ({ type: FETCH_USER_FULFILLED, payload });
const cancelFetchUser = () => ({ type: FETCH_USER_CANCELLED });
const fakeAjax = url => of({
id: url.substring(url.lastIndexOf('/') + 1),
firstName: 'Bilbo',
lastName: 'Baggins'
}).pipe(delay(1000));
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action => fakeAjax(`/api/users/${action.payload}`).pipe(
map(response => fetchUserFulfilled(response)),
takeUntil(action$.pipe(
filter(action => action.type === FETCH_USER_CANCELLED)
))
))
);
const users = (state = {}, action) => {
switch (action.type) {
case FETCH_USER:
return {};
case FETCH_USER_FULFILLED:
return {
...state,
[action.payload.id]: action.payload
};
default:
return state;
}
};
const isFetchingUser = (state = false, action) => {
switch (action.type) {
case FETCH_USER:
return true;
case FETCH_USER_FULFILLED:
case FETCH_USER_CANCELLED:
return false;
default:
return state;
}
};
I want replace fetchAjax use axios
const fakeAjax = url,params =>{ return axios({
method: 'post',
url: url,
data: params
});
}
I don't understand the added value of using axios, since ajax from rxjs will simplify your code (it is already using observables). However, if you really want to it is definitely possible. I assume in the example below that you are using actions where the payload consists of a url and request data.
const fetchUserEpic = action$ => action$.pipe(
ofType(FETCH_USER),
mergeMap(action => from(axios({method: 'get', action.payload.url, data: action.payload.data})).pipe(
map(response => fetchUserFulfilled(response)),
takeUntil(action$.ofType(FETCH_USER_CANCELLED)),
))
);
Also: keep in mind that cancelling will prevent the redux store from being updated, but it will not cancel the axios request from being processed.