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
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));
}
}
}
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
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.
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;
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?