Thunk is fulfilled before array map finishes - reactjs

I have a thunk which fetches data from firestore and maps doc IDs to each doc. For some reason it's fulfilled and returns undefined before the mapping finishes. I can verify this because the log before return statement appears a second or two after the fulfilled reducer logs.
My thunk:
export const fetchNotesByCustomerId = createAsyncThunk(
'fetchNotesByCustomerId',
async (custId, { getState, dispatch }) => {
const customerId = custId;
if (customerId !== undefined) {
notesService
.fetchNotesByCustomerId(customerId)
.then((snapshot) => {
if (snapshot.docs.length !== 0) {
const notes = snapshot.docs.map(
(doc) =>
({
...doc.data(),
docId: doc.id
} as INote)
);
console.log('notes inside thunk: ', notes); // This shows after the fulfilled reducer logs
return {
error: false,
data: notes
};
} else
return {
error: true,
message: 'No notes found in firebase',
data: []
};
})
.catch((error) => {
return { error: true, message: error.message, data: [] };
});
} else return { error: true, message: 'No Customer Id' };
}
);
I know .map is an async function that returns an array of promises, but when I use await, intellisense notifies me that it makes no difference on the behavior of the function.
So as an alternative, I tried to resolve the array of promises like this, but saw no difference:
.then(async (snapshot) => {
if (snapshot.docs.length !== 0) {
const notesPromisesArray = snapshot.docs.map(
(doc) =>
({
...doc.data(),
docId: doc.id
} as INote)
);
await Promise.all(notesPromisesArray).then((notes) => {
console.log('notes inside thunk: ', notes);
return {
error: false,
data: notes
};
});
} else
return {
error: true,
message: 'No notes found in firebase',
data: []
};
})
How can I get this .map to return before the thunk is fulfilled?

The problem you're facing has nothing to do with Redux Async Thunk but basic JavaScript only.
In the if condition that you have:
if (customerId !== undefined) {
notesService
.fetchNotesByCustomerId(customerId)
.then((snapshot) => {
if (snapshot.docs.length !== 0) {
const notes = snapshot.docs.map(
(doc) =>
({
...doc.data(),
docId: doc.id,
} as INote)
)
console.log('notes inside thunk: ', notes) // This shows after the fulfilled reducer logs
return {
error: false,
data: notes,
}
} else
return {
error: true,
message: 'No notes found in firebase',
data: [],
}
})
.catch((error) => {
return { error: true, message: error.message, data: [] }
})
}
You're using noteService.fetchNotesByCustomerId() which is an async function.
When the execution goes to this block, since JavaScript Event loop will forward the async function to its thread pool, it goes to the next step without even the execution of noteService.fetchNotesByCustomerId() getting over and resolves the thunk without returning anything.
You can easily resolve this by adding a return statement next to your call:
export const fetchNotesByCustomerId = createAsyncThunk('fetchNotesByCustomerId', async (custId, { getState, dispatch }) => {
const customerId = custId
if (customerId !== undefined) {
return notesService
.fetchNotesByCustomerId(customerId)
.then((snapshot) => {
if (snapshot.docs.length !== 0) {
const notes = snapshot.docs.map(
(doc) =>
({
...doc.data(),
docId: doc.id,
} as INote)
)
console.log('notes inside thunk: ', notes) // This shows after the fulfilled reducer logs
return {
error: false,
data: notes,
}
} else
return {
error: true,
message: 'No notes found in firebase',
data: [],
}
})
.catch((error) => {
return { error: true, message: error.message, data: [] }
})
} else return { error: true, message: 'No Customer Id' }
})

Related

props becomes undefined on page refresh which resist socket re-establishment

I have implemented the socket, which was not able to established after pressing F5 button,
when users logged into the app, socket has been establishes successfully, when I hit F5, page refreshes but socketConnection is not invoked because props is undefined due to which conditional statement has been failed to execute.
After inspecting I have found that, I invoked the main socket creation function inside App.js, it takes props as as argument and one conditional statement. I have found that the props itself is undefined due to which the socket i not able t re-established.
App.js
import React, { useEffect } from 'react';
import './App.scss';
import Routes from './Routes';
import { connect } from 'react-redux';
import socketConnection from '#Hoc/SocketComponent';
const App = () => {
useEffect((props) => {
if (props?.session?.user?.extraDetails?.extensionNo) {
socketConnection(props);
}
}, []);
return (
<div>
<Routes />
</div>
);
};
const mapStateToProps = (state) => ({
session: state.session
});
export default connect(mapStateToProps)(App);
SocketComponent
export default function socketConnection(socketEvent) {
if (!window?.socket)
sessionService.loadUser().then((currentUser) => {
if (
currentUser?.extraDetails?.extensionNo &&
currentUser?.params?.AgentID
)
socket = window.socket = io(REACT_APP_SOCKET_URL, {
query: {
agentId: currentUser.params.AgentID,
extensionNo: currentUser.extraDetails.extensionNo,
},
});
socket.on("connect", (data) => {
console.info(data);
});
socket.on("EventEstablished", (data) => {
eventEstablished(data, currentUser, socketEvent);
});
socket.on("EventAgentLogout", () => {
notification.error({
message: "Softphone loggedout, please re-login",
duration: 0,
});
socketEvent.history.push("/home");
});
socket.on("EventPropertiesChanged", (data) => {
manualGeocode(data);
});
socket.on("EventAttachedDataChanged", (data) => {
if (data.data?.incidentNumber) {
store.dispatch({
type: SET_LIVE_CALL_DATA,
payLoad: { psapReferenceId: data?.data?.incidentNumber },
});
}
if (data.data?.updateLocation && data.data?.isRetransmit) {
let functionCall = "retransmit" ;
// if (data.data?.isRetransmit) {
// functionCall = "retransmit" ;
// }
getCallData( functionCall );
}
// manualGeocode(data);
});
socket.on("EventDestinationBusy", (data) => {
console.log("EventDestinationBusy", data);
});
socket.on("EventAbandoned", (data) => {
notification.error({
message: "Call abandoned",
description: "The caller abandoned the call before it was answered",
duration: 0,
});
});
socket.on("EventDNOutOfService", (data) => {
notification.error({
message: "Extension Out of Service !",
description:
"This extension is out of service and cannot make or receive calls. ",
duration: 0,
});
});
socket.on("EventAgentReady", (data) => {
console.log("EventAgentReady", data);
});
socket.on("EventAgentNotReady", (data) => {
console.log("EventAgentNotReady", data);
});
socket.on("EventReleased", (data) => {
eventReleased(data);
});
socket.on("EventPartyDeleted", (data) => {
eventPartyDeleted(data);
});
socket.on("EventInvite", (data) => {
console.log(data);
if (
!store?.getState()?.secondAgent?.secondAgent?.isSecondAgent &&
socketEvent.history.location.pathname === "/waiting"
) {
eventInvite(data, currentUser, socketEvent);
}
});
socket.on("disconnect", (data) => {
console.log(data);
});
socket.on("workflow", (data) => {
store.dispatch({ type: SET_WORKFLOW_DATA, payLoad: data });
});
socket.on("workflowUpdatedComponent", (data) => {
store.dispatch({ type: SET_WORKFLOW_OPTIONS, payLoad: data });
});
socket.on("gather-info", (data) => {
console.log(data);
if (data?.data?.extensionNo != currentUser.extraDetails.extensionNo) {
store.dispatch({
type: SET_GATHER_INFO,
payLoad: data?.data?.gatherInfo,
});
}
});
socket.on("geoCodeSession", (data) => {
console.log(data);
if (data?.data?.extensionNo != currentUser.extraDetails.extensionNo) {
if (data?.data?.updateLocation) {
store.dispatch({
type: SET_MANUAL_GEOCODE,
payLoad: { updateLocation: true },
});
}
// let timeFrame = new Date(data.data.timestamp);
// timestamp = timeFrame.toLocaleDateString() + ' ' + timeFrame.toLocaleTimeString();
store.dispatch({
type: SET_MANUAL_GEOCODE,
payLoad: {
status: true,
latitude: data?.data?.latitude,
longitude: data?.data?.longitude,
address: data?.data?.address,
country: data?.data?.country,
region: data?.data?.region,
timestamp: data?.data?.timestamp,
},
});
}
});
socket.on("EventSessionInfo", (data) => {
if (data?.data?.extensionNo !== currentUser?.extraDetails.extensionNo) {
if (data.data.sessionStatus === "Over") {
store.dispatch({
type: SET_SECOND_AGENT,
payLoad: { status: "Disconnected", isSecondAgent: false },
});
} else if (
data.data.sessionStatus === "Alive" &&
data.data.agentNameChat
) {
store.dispatch({
type: SET_SECOND_AGENT,
payLoad: {
isSecondAgent: true,
status: "Connected",
anotherAgent: data.data.messageText,
isSecondAgent: true,
},
});
} else if (
data.data.sessionStatus === "Alive" &&
!data.data.messageText.includes("Leaving ChatRoom..") &&
!data.data.messageText.includes("Join Chat Session")
) {
chatStore(data);
}
}
});
});
}
I am not able to figured it out what went wrong, however I am trying to load when props are there but not able to do the same.
PROBLEM
It seems like props are not loaded while execution of `useEffect
Solution might be require some kind of delay to useEffect, so once props properly loaded form localstorage

problems in request in unit test the status is undefined

I am trying to do a unit test to an action in my react application but apparently everything works fine, I get a message that I am not understanding when making the request and the status is undefined, I don't have any specific variable with the status name so I assume it must be a problem when making the promise. How can I solve this problem?
error : undefined | TypeError: Cannot read property 'status' of
undefined
at request (C:\Users\amils\OneDrive\Documentos\Bootcamp - Training\Project\tracking-tool-webapp\src\api\utilities\fetch.js:45:26)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at Object.getAll (C:\Users\amils\OneDrive\Documentos\Bootcamp -
Training\Project\tracking-tool-webapp\src\api\utilities\provider.js:18:9)
console log result:
console.log
[
{
title: 'Candidates',
errorMessages: [],
candidates: [],
reports: null,
loading: false,
programsInProgress: [],
programVersions: [],
statusType: []
},
{ onLoadCandidates: [Function: onLoadCandidates] }
]
The code:
it('Should get all candidates', async () => {
const mockResponse = {
candidates: [
{
id: '4fffc534-1d83-14d5-b264-1e17f2abd322',
name: 'Homer Simpson',
status: 'InProgress',
},
{
id: '4fffc535-1d83-14d5-b264-1e17f2abd322',
name: 'Junior Santos',
status: 'InProgress',
},
],
};
global.fetch = jest.fn(() => {
Promise.resolve({
status: 200,
json: () => Promise.resolve(mockResponse),
});
});
const result = await customRenderHook();
const actions = result.current[1];
console.log(result);
await act(async () => {
actions.onLoadCandidates();
});
const state = result.current[0];
expect(state.candidates).toEqual(mockResponse);
});
code customRenderHook:
const customRenderHook = () => {
const wrapper = ({ children }) => <CandidatesDataProvider>{children}</CandidatesDataProvider>;
const { result } = renderHook(() => useCandidatesContext(), { wrapper });
return result;
};
I find the problem, currently, I cant execure my promise without a tokes 'Bearer', now the problem here is how can I create a mock of token:
function onLoadCandidates(dispatch) {
dispatch({ type: CandidatesActionTypes.loading, payload: true });
const token = localStorage.getItem('token');
apiCandidate
.getAll(token)
.then((response) => {
dispatch({ type: CandidatesActionTypes.loading, payload: response.data });
})
.catch((err) => {
dispatch({ type: CandidatesActionTypes.Error, payload: err.message });
LoggerService.error(err);
})
.finally(() => {
dispatch({ type: CandidatesActionTypes.loading, payload: false });
});
}
You could mock localStorage.getItem to return a token in the format required by apiCandidate.getAll(token):
localStorage.getItem = jest.fn().mockReturnValue('your-token');

Redux state updated but component not re-rendered (while using promise)

I am using React/Redux.
The main issue is that when i use Promise then component is not re-rendered, whereas the code is working fine when promise code is not used.
Action Creator
const updateColor = colorobj => {
return dispatch =>
new Promise(function(resolve, reject) {
dispatch(fetchColorBegin());
axios
.post(config.APIURL.color.update, colorobj)
.then(response => {
const data = response.data;
if (data.errorno !== 0) {
dispatch(fetchColorFailure(data.errormsg));
reject(data.errormsg);
} else {
dispatch(updateColorSuccess(colorobj));
resolve('Color Updated');
}
})
.catch(error => {
dispatch(fetchColorFailure(error.message));
reject(error.message);
});
});
};
Reducer
case UPDATE_COLOR_SUCCESS:
const todoIndex = state.data.findIndex(todo => todo.id === action.payload.id);
return update(state, {
loading: { $set: false },
data: { [todoIndex]: { $merge: action.payload } },
error: { $set: null}
});
Component
the state is updated but the component is not updated.
const handleEditOk = values => {
let colorobj = {
id: state.updateData.id,
colorname: values.colorname,
colorcode: values.colorcode,
};
dispatch(updateColor(colorobj))
.then(response => {
message.success(response);
onCancel();
})
.catch(error => {
message.error(error);
});
};
The component update itself only on commenting the promise code.
The problem now is that it is not showing success/failure message.
const handleEditOk = values => {
let colorobj = {
id: state.updateData.id,
colorname: values.colorname,
colorcode: values.colorcode,
};
dispatch(updateColor(colorobj))
// .then(response => {
// message.success(response);
// onCancel();
// })
// .catch(error => {
// message.error(error);
// });
};
Kindly suggest.

useEffect infinite loop occurs only while testing, not otherwise - despite using useReducer

I'm trying to test a useFetch custom hook. This is the hook:
import React from 'react';
function fetchReducer(state, action) {
if (action.type === `fetch`) {
return {
...state,
loading: true,
};
} else if (action.type === `success`) {
return {
data: action.data,
error: null,
loading: false,
};
} else if (action.type === `error`) {
return {
...state,
error: action.error,
loading: false,
};
} else {
throw new Error(
`Hello! This function doesn't support the action you're trying to do.`
);
}
}
export default function useFetch(url, options) {
const [state, dispatch] = React.useReducer(fetchReducer, {
data: null,
error: null,
loading: true,
});
React.useEffect(() => {
dispatch({ type: 'fetch' });
fetch(url, options)
.then((response) => response.json())
.then((data) => dispatch({ type: 'success', data }))
.catch((error) => {
dispatch({ type: 'error', error });
});
}, [url, options]);
return {
loading: state.loading,
data: state.data,
error: state.error,
};
}
This is the test
import useFetch from "./useFetch";
import { renderHook } from "#testing-library/react-hooks";
import { server, rest } from "../mocks/server";
function getAPIbegin() {
return renderHook(() =>
useFetch(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
{ method: "GET" },
1
)
);
}
test("fetch should return the right data", async () => {
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
// Overwrite mock with failure case
test("shows server error if the request fails", async () => {
server.use(
rest.get(
"http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin",
async (req, res, ctx) => {
return res(ctx.status(500));
}
)
);
const { result, waitForNextUpdate } = getAPIbegin();
expect(result.current.loading).toBe(true);
expect(result.current.error).toBe(null);
expect(result.current.data).toBe(null);
await waitForNextUpdate();
console.log(result.current);
expect(result.current.loading).toBe(false);
expect(result.current.error).not.toBe(null);
expect(result.current.data).toBe(null);
});
I keep getting an error only when running the test:
"Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."
The error is coming from TestHook: node_modules/#testing-library/react-hooks/lib/index.js:21:23)
at Suspense
I can't figure out how to fix this. URL and options have to be in the dependency array, and running the useEffect doesn't change them, so I don't get why it's causing this loop. When I took them out of the array, the test worked, but I need the effect to run again when those things change.
Any ideas?
Try this.
function getAPIbegin(url, options) {
return renderHook(() =>
useFetch(url, options)
);
}
test("fetch should return the right data", async () => {
const url = "http://fe-interview-api-dev.ap-southeast-2.elasticbeanstalk.com/api/begin";
const options = { method: "GET" };
const { result, waitForNextUpdate } = getAPIbegin(url, options);
expect(result.current.loading).toBe(true);
await waitForNextUpdate();
expect(result.current.loading).toBe(false);
const response = result.current.data.question;
expect(response.answers[2]).toBe("i think so");
});
I haven't used react-hooks-testing-library, but my guess is that whenever React is rendered, the callback send to RenderHook will be called repeatedly, causing different options to be passed in each time.

Delete item using redux

I am trying to get my head around Redux. Doing something like TODO APP with React and Redux. I can add a new task, update its value, but I cannot delete the item correctly. I get an error Unhandled Rejection (TypeError): Cannot read property 'id' of undefined all the time. I pass the ID to the Delete function just like I do in the Update function. Server side works well. The fetch function itself works, and the delete of item from the database works, but an error occurs on the client side Help please guys
const initialState = {
list: [],
}
export default (state = initialState, action) => {
switch (action.type) {
case GET_TASKS: {
return { ...state, list: action.tasks }
}
case CREATE_TASK: {
return { ...state, list: [...state.list, action.task] }
}
case UPDATE_STATUS: {
return {
...state,
list: state.list.map((it) => {
return action.task.id === it.id ? action.task : it
}),
}
}
case DELETE_TASK: {
return {
list: state.list.map((it) => {
return action.task.id !== it.id ? action.task : it
}),
}
}
default:
return state
}
}
export function getTasks() {
return (dispatch) => {
fetch("/api/task")
.then((r) => r.json())
.then(({ data: tasks }) => {
dispatch({ type: GET_TASKS, tasks })
})
}
}
export function deleteTask(id) {
return (dispatch) => {
fetch(`/api/v1/task/${id}`, {
method: "DELETE",
})
.then((r) => r.json())
.then(({ data: task }) => {
dispatch({ type: DELETE_TASK, task })
})
}
}
My first question would be, in your deleteTask method what is being returned here? Does a delete method actually return the task you deleted?
.then(({ data: task }) => {
dispatch({ type: DELETE_TASK, task })
}
If not, another way you can address this is by changing the task in your dispatch to the id you are passing to the deleteTask method:
dispatch({ type: DELETE_TASK, id });
Then use the filter method instead of map in your reducer to return the tasks that don't match that deleted task's id, effectively "deleting" it from your state:
case DELETE_TASK: {
return {
list: state.list.filter((it) => {
return action.id !== it.id;
}),
}
}

Resources