Catch the errors from the thunk into the functional components React - reactjs

How can I catch the errors from async method in the thunk into the Functional Components?
For example I have the following thunk:
export const updateCostCenter = (data: Record<string, unknown>) => async (dispatch: Dispatch<IWorkforceState>) => {
dispatch(requestUpdateCostCenter());
return api('put', `${costCenterUrl}/${data.rowId}`, data)
.then(response => {
return dispatch(receiveUpdateCostCenter(response.data));
})
.catch(err => {
return dispatch(errorUpdateCostCenter(err.response?.data?.description));
});
};
and in the functional component the following asynchronous method that calls the thunk:
props.updateCostCenter(valueToSubmit).then(
() => {
props.showToastNotification('success', 'Successful', props.translate('cost_center_successfully_updated'));
AmplitudeService.logEvent(props.translate('edit_cost_center'));
props.hideDialog();
resetForm();
setSubmitting(false);
if (props.loadData) {
props.loadData();
}
return
}
).catch(() => {
props.showToastNotification('error', 'Error', props.translate('cost_center_update_error'))
});
Unfortunately, I don't know why in case of error it doesn't enter into the catch from the functional component. I tried to add throw TypeError() after the dispatch of the error action, it works, but the thunk unit test fails on the pipeline.
This are the tests:
it('update cost center success', function() {
mockAdd.mockImplementation(
() =>
Promise.resolve({
data: costCenter,
} as any)
);
const expectedActions = [
{ type: WorkforceActions.REQUEST_UPDATE_COST_CENTER },
{ type: WorkforceActions.RECEIVE_UPDATE_COST_CENTER, costCenter },
];
store.dispatch(updateCostCenter({ data: costCenter }) as any).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(api).toHaveBeenCalled();
return
}).catch((unexpectedErr: any) => console.log(`Unexpectedly rejected promise ${unexpectedErr}`));
});
it('update cost center error', function() {
mockAdd.mockImplementation(
() =>
Promise.reject({
response: { data: { description: 'dummy-message' } },
} as any)
);
const expectedActions = [
{ type: WorkforceActions.REQUEST_UPDATE_COST_CENTER },
{ type: WorkforceActions.ERROR_UPDATE_COST_CENTER, message: 'dummy-message' },
];
store.dispatch(updateCostCenter({ data: costCenter }) as any).catch(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(api).toHaveBeenCalled();
});
});

Because you don't return an error.
export const updateCostCenter = (data: Record<string, unknown>) => async (dispatch: Dispatch<IWorkforceState>) => {
dispatch(requestUpdateCostCenter());
return api('put', `${costCenterUrl}/${data.rowId}`, data)
.then(response => {
dispatch(receiveUpdateCostCenter(response.data));
return response;
})
.catch(err => {
dispatch(errorUpdateCostCenter(err.response?.data?.description));
throw err; // or throw new Error();
});
};

Related

How to use PATCH method in react.js and typescript using react-query

I want to use the PATCH method to update a list of items instead of PUT. I have be able to remove any CORS blockers. The issue is the response i receive when i update is null and all the forms are then replaced.
task.service.ts
const update = async (id: any, { TaskName, TaskDescription, TaskOwner, TaskStatus, Skills, InitiativeLink, InitiativeName, TimeCommitment }: Task) => {
const response = await apiClient.patch<any>(`/tasks/${id}`, { TaskName, TaskDescription, TaskOwner, TaskStatus, Skills, InitiativeLink, InitiativeName, TimeCommitment });
return response.data;
};
OwnerHome.tsx
const { isLoading: isUpdatingTask, mutate: updateTask } = useMutation(
(putId: string) => {
return TaskService.update(
putId,
{
TaskName: putName,
InitiativeName: putInitiativeName,
TaskDescription: putDescription,
TaskOwner: putOwner,
InitiativeLink: putInitiativeLink,
Skills: putSkills,
TimeCommitment: putTimeCommitment,
TaskStatus: putStatus,
});
},
{
onSuccess: (res) => {
setPutResult(fortmatResponse(res));
},
onError: (err: any) => {
setPutResult(fortmatResponse(err.response?.data || err));
},
},
);
useEffect(() => {
if (isUpdatingTask) setGetResult('updating...');
}, [isUpdatingTask]);
function putData() {
if (selectedItems[0]) {
try {
updateTask(selectedItems[0].ID);
// setVisible(true);
} catch (err) {
setPutResult(fortmatResponse(err));
}
}
}

React redux Sagas, wait for a Saga to set state

I have some component with a code like this:
const startLogin = (code) => {
dispatch(login({ code }));
const publicKeyFromLocalST = window.localStorage.getItem('push_public_key');
setPublicKey(publicKeyFromLocalST);
// etc
When I dispatch the saga login it will store some data in localStorage.
I need to execute the 3rd line (setPublicKey) after that data be actually indeed in localStorage.
How can "await" for dispatch(login({ code })); to be completed before setPublicKey?
Two options:
Execute the setPublicKey function inside the worker saga, you can control the workflow in the worker saga easily with yield.
function* login(action) {
const response = yield call(apiCall);
if (response.error) {
yield put({ type: actionType.LOGIN_FAIL });
} else {
yield put({ type: actionType.LOGIN_SUCCESS, data: response.data });
const publicKeyFromLocalST = window.localStorage.getItem('push_public_key');
setPublicKey(publicKeyFromLocalST);
}
}
Promisify the dispatch(login({code})), you should create a helper function like this:
const loginAsyncCreator = (dispatch) => (payload) => {
return new Promise((resolve, reject) => dispatch(loginCreator(payload, { resolve, reject })));
};
You need to pass the resolve/reject to worker saga via action.meta, then you can decide when to resolve or reject the promise. Then, you can use async/await in your event handler. See below example:
import { call, put, takeLatest } from 'redux-saga/effects';
import { createStoreWithSaga } from '../../utils';
const actionType = {
LOGIN: 'LOGIN',
LOGIN_FAIL: 'LOGIN_FAIL',
LOGIN_SUCCESS: 'LOGIN_SUCCESS',
};
function apiCall() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ error: null, data: 'login success' });
}, 2000);
});
}
function* login(action) {
console.log('action: ', action);
const {
meta: { resolve, reject },
} = action;
const response = yield call(apiCall);
console.log('response: ', response);
if (response.error) {
yield put({ type: actionType.LOGIN_FAIL });
yield call(reject, response.error);
} else {
yield put({ type: actionType.LOGIN_SUCCESS, data: response.data });
yield call(resolve, response.data);
}
}
function* watchLogin() {
yield takeLatest(actionType.LOGIN, login);
}
const store = createStoreWithSaga(watchLogin);
function loginCreator(payload, meta) {
return {
type: actionType.LOGIN,
payload,
meta,
};
}
const loginAsyncCreator = (dispatch) => (payload) => {
return new Promise((resolve, reject) => dispatch(loginCreator(payload, { resolve, reject })));
};
const loginAsync = loginAsyncCreator(store.dispatch);
async function startLogin() {
await loginAsync({ code: '1' });
console.log('setPublicKey');
}
startLogin();
The logs:
action: {
type: 'LOGIN',
payload: { code: '1' },
meta: { resolve: [Function (anonymous)], reject: [Function (anonymous)] }
}
response: { error: null, data: 'login success' }
setPublicKey

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.

twilio device connect and reconnect

I am trying to create a context api for twilio call connection. I would like to take help from twilio experts if I am doing it in correct way or not. In this context, What I am trying to do is connect twilio device and if the token expires then reconnect. But this way, the outgoing call is not working yet I could see the device is ready printed on my console. When I try to call, the deviceInstance state is shown null.
This is how I am doing
import { Device, Connection } from 'twilio-client';
import twilioReducers from './twilioReducers';
interface ITwilioContext {
// device?: Device;
setDeviceOnline?: () => void;
state: InitialStateType;
dispatch: React.Dispatch<any>;
handleDeviceOutgoing: (params: OutgoingProps) => void;
}
export const TwilioContext = createContext<ITwilioContext>({});
const TwilioProvider = ({ children }: { children: React.ReactNode }) => {
const [deviceInstance, setDeviceInstance] = useState<Device | null>(null);
const [activeWorkspaceId] = useLocalStorage('activeWorkspaceId', null);
const { t } = useTranslation();
const [getVoiceToken, { data }] = useLazyQuery(VOICE_TOKEN, {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
onError: err => console.error(err),
});
// Commented inorder to prevent reintialization
// const device = new Device();
const initialState = { direction: '', showPhoneWidget: false };
const [state, dispatch] = useReducer(twilioReducers, initialState);
/* event handlers for twilio device */
const handleDeviceReady = (device: Device) => {
console.log('Device ready');
setDeviceInstance(device);
};
const handleDeviceOffline = async (device: Device) => {
console.log('Device offline', device);
if (device.token) {
const twilioTokenDecoded = jwtDecode<JwtPayload>(device.token);
if ((twilioTokenDecoded as any).exp <= Date.now() / 1000) {
await getVoiceToken({});
console.log('twilio new token data', data);
}
}
};
const handleDeviceError = (error: Connection.Error) => {
console.log('Device error', error);
if (TWILIO_ERRORS[error.code] !== undefined) {
ToastMessage({
content: t(TWILIO_ERRORS[error.code].errorKey, TWILIO_ERRORS[error.code].message),
type: 'danger',
});
}
};
/* ----------------------------- */
/* handle incoming calls */
const handleDeviceIncoming = (connection: Connection) => {
console.log('incoming', connection);
dispatch({
type: ACTIONS.INCOMING_CALL,
data: connection,
});
connection.on(deviceEvent.CANCEL, () => {
console.log('incoming-cancel');
dispatch({
type: ACTIONS.INCOMING_CALL_CANCEL,
});
});
connection.on(deviceEvent.DISCONNECT, () => {
console.log('incoming-disconnect');
dispatch({
type: ACTIONS.INCOMING_CALL_DISCONNECT,
});
});
connection.on(deviceEvent.ACCEPT, () => {
console.log('incoming-call-accept');
dispatch({
type: ACTIONS.ANSWER_INCOMING_CALL,
});
});
connection.on(deviceEvent.REJECT, () => {
console.log('incoming-call-reject');
dispatch({
type: ACTIONS.REJECT_INCOMING_CALL,
updateConversationStatus: true,
});
});
connection.on(deviceEvent.ERROR, (err: Connection.Error) => {
console.log('Connection error occured', err);
dispatch({
type: ACTIONS.INCOMING_CALL_ERROR,
status: 'error',
});
});
};
/* ----------------------------- */
/* handle outgoing calls */
const handleDeviceOutgoing = (params: OutgoingProps) => {
if (deviceInstance) {
if (deviceInstance.isInitialized || deviceInstance.status() !== 'ready') {
ToastMessage({ content: t('error.deviceSetup', 'Device is offline.'), type: 'danger' });
return;
}
const connection = deviceInstance.connect(params); // copied from premvp
dispatch({
type: ACTIONS.OUTGOING_CALL_INITIATED,
data: connection,
status: 'connecting',
channelId: params?.channel_sid,
});
connection.on(deviceEvent.RINGING, (val: boolean) => {
if (val) {
dispatch({
type: ACTIONS.OUTGOING_CALL_RINGING,
});
}
});
connection.on(deviceEvent.CANCEL, () => {
console.log('Connection cancelled');
dispatch({
type: ACTIONS.OUTGOING_CALL_DISCONNECT,
});
});
connection.on(deviceEvent.DISCONNECT, (conn: Connection) => {
// handle user hungup
console.log('Connection disconnected', conn);
dispatch({
type: ACTIONS.OUTGOING_CALL_DISCONNECT,
});
});
connection.on(deviceEvent.ACCEPT, (conn: Connection) => {
console.log('Connected to the user', conn); // handle user answercall
dispatch({
type: ACTIONS.OUTGOING_CALL_ANSWERED,
});
});
connection.on(deviceEvent.REJECT, (conn: Connection) => {
console.log('Rejected', conn); // handle user answercall
dispatch({
type: ACTIONS.REJECT_OUTGOING_CALL,
});
});
connection.on(deviceEvent.ERROR, (err: Connection.Error) => {
console.log('Connection error occured', err);
});
} else {
console.log('No Device Instance exist');
}
};
/* ----------------------------- */
useEffect(() => {
const device = new Device();
console.log('device', device, data);
if (data?.getVoiceToken?.data?.voiceToken) {
device.setup(data?.getVoiceToken?.data?.voiceToken, deviceConfig);
device.on(deviceEvent.READY, handleDeviceReady);
device.on(deviceEvent.OFFLINE, handleDeviceOffline);
device.on(deviceEvent.ERROR, handleDeviceError);
device.on(deviceEvent.INCOMING, handleDeviceIncoming);
}
return () => {
device.destroy();
setDeviceInstance(null);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.getVoiceToken?.data?.voiceToken]);
useEffect(() => {
if (activeWorkspaceId !== '') {
getVoiceToken({});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeWorkspaceId]);
const value = useMemo(() => {
return {
state,
dispatch,
deviceInstance,
handleDeviceOutgoing,
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
return <TwilioContext.Provider value={value}>{children}</TwilioContext.Provider>;
};
export default TwilioProvider;

How to test custom hooks with event listener inside useEffect?

I'm using react native and jest to create my tests. I'm facing problems to test an event listener that listens to url events from expo-linking. This event listenner is inside an useEffect hook.
Below is the code from my custom hook with my useEffect and an event listener inside:
useEffect(() => {
isMounted.current = true;
Linking.addEventListener('url', async () => {
try {
if (!navigation.isFocused() || !isMounted.current) return;
setIsLoading(true);
const response = await api.get('sessions/auth/success');
if (!response.data) return;
console.log('aqui');
const { notRegisteredUser, token } = response.data;
api.defaults.headers.authorization = `Bearer ${token}`;
if (notRegisteredUser && token) {
setIsLoading(false);
navigation.navigate('BirthDateScreen');
dispatch(
updateUser({
...notRegisteredUser,
}),
);
}
} catch (err) {
Alert.alert('Error', `${translate('loginRegisterError')}: `, err);
}
});
return () => {
isMounted.current = false;
};
}, [dispatch, navigation]);
In my test file I have the following mocks:
jest.mock('expo-linking', () => {
return {
addEventListener: (event: string, callback: () => void) => callback(),
};
});
jest.mock('#react-navigation/native', () => {
return {
useNavigation: () => ({
isFocused: mockedNavigationFocus,
navigate: mockedNavigation,
}),
};
});
jest.mock('react-redux', () => {
return {
useDispatch: jest.fn(),
};
});
jest.mock('../../../store/modules/user/actions', () => {
return {
updateUser: jest.fn(),
};
});
jest.mock('i18n-js', () => {
return {
locale: 'en',
t: () => 'any key',
};
});
Finally this is how my test looks in my first try:
it('should pass the test', async done => {
mockedNavigationFocus.mockImplementation(() => true);
apiMock.onGet('sessions/auth/success').reply(200, {
notRegisteredUser: { name: 'Logan' },
token: '123',
});
render(<LoginScreen />);
await waitFor(() =>
expect(mockedNavigation).toHaveBeenCalledWith('BirthDateScreen'),
);
done();
});
In my second try this is how my test looked (I used renderHooks from #testing-library/react-hooks):
it('should pass the test', async () => {
mockedNavigationFocus.mockImplementation(() => true);
apiMock.onGet('sessions/auth/success').reply(200, {
notRegisteredUser: { name: 'Logan' },
token: '123',
});
const { result, waitForValueToChange } = renderHook(() => useLoginButton());
const { isLoading } = result.current;
await waitForValueToChange(() => isLoading);
await waitForValueToChange(() => isLoading);
expect(mockedNavigation).toHaveBeenCalledWith('BirthDateScreen');
});
With both tests I get the following error:
test error
Another error I get is that my callback function inside useEffect runs many times before it stops and this does not happen when I am not testing.
Does anyone knows how can I write this test?

Resources