Proper error handling using Redux Toolkit - javascript

Say for example when a login function calls an API and it returns an error because of something like invalid credentials. I have noticed that it still goes through the fulfilled case in the extra reducers part. Should I add an if statement to check if response code is 200 or is there a way for the thunk to go through the rejected case?
extraReducers: builder => {
builder.addCase(login.pending, (state, action) => {
state.fetchingError = null;
state.fetchingUser = true;
});
builder.addCase(login.fulfilled, (state, {payload}) => {
console.log(payload, 'hello?');
state.user = payload.data.user;
});
builder.addCase(login.rejected, (state, action) => {
state.fetchingUser = false;
state.fetchingError = action.error;
});
},

You can use rejectWithValue in createAsyncThunk to customize the reject action.
It also takes an argument which will be "action.payload" in the reject action.
In createAsyncThunk:
const updateUser = createAsyncThunk(
'users/update',
async (userData, { rejectWithValue }) => {
const { id, ...fields } = userData
try {
const response = await userAPI.updateById(id, fields)
return response.data.user
} catch (err) {
// Use `err.response.data` as `action.payload` for a `rejected` action,
// by explicitly returning it using the `rejectWithValue()` utility
return rejectWithValue(err.response.data)
}
}
)
https://redux-toolkit.js.org/api/createAsyncThunk#handling-thunk-errors

Related

How to show reject message with createAsyncThunk?

I want to show an error message when my API fetch throws an error but this error actually gets fulfilled so in the extra reducers, the rejected part doesn't get invoked at all.
export const searchWeather = createAsyncThunk(
"weather/searchWeather",
async (apiAddress) => {
const response = await fetch(apiAddress);
const data = await response.json();
return data;
}
);
................................................................................
extraReducers: (builder) => {
builder
.addCase(searchWeather.pending, (state) => {
state.isLoading = true;
state.hasError = false;
})
.addCase(searchWeather.fulfilled, (state, action) => {
state.weather = action.payload;
state.isLoading = false;
state.hasError = false;
})
.addCase(searchWeather.rejected, (state) => {
state.isLoading = false;
state.hasError = true;
});
},
In this way even if I get a 404 Error, it still does get fulfilled and not rejected.
What I did was to include Promise.reject() in my async function in this way:
export const searchWeather = createAsyncThunk(
"weather/searchWeather",
async (apiAddress) => {
const response = await fetch(apiAddress);
if (!response.ok) {
return Promise.reject();
}
const data = await response.json();
return data;
}
);
And it indeed does work without any problems and shows the error message I've defined somewhere else.
But I want to know if this is actually the right way to do it or if there is a better solution to this.
This is pretty much the correct way - fetch does not throw on a non-2xx-status code.
Sementics can change a bit - you would usually either throw or return rejectWithValue:
if (!response.ok) {
throw new Error("response was not ok")
}
or
if (!response.ok) {
return thunkApi.rejectWithValue("response was not okay")
}

Async does wait for data to be returned in a redux-thunk function

I've being trying populate my redux store with data that comes from my mongo-db realm database.
Whenever I run the function below it will execute fine but the problem is data will be delayed and ends up not reaching my redux store.
My thunk function:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
try {
return await Realm.open(config).then(async (projectRealm) => {
let syncItems = await projectRealm.objects("Item");
await syncItems.addListener((x, changes) => {
x.map(async (b) => {
console.log(b);
return b;
});
});
});
} catch (error) {
console.log(error);
throw error;
}
}
);
and my redux reducer:
extraReducers: (builder) => {
builder.addCase(addItemsTest.fulfilled, (state, { payload }: any) => {
try {
console.log("from add Items");
console.log(payload);
state.push(payload);
} catch (error) {
console.log(error)
}
});
}
Expected Results:
My redux store should have these data once addItemsTest return something:
[{
itemCode: 1,
itemDescription: 'Soccer Ball',
itemPrice: '35',
partition: 'partitionValue',
},
{
itemCode: 2,
itemDescription: 'Base Ball',
itemPrice: '60',
partition: 'partitionValue',
}
]
Actual Results:
Mixed Syntaxes
You are combining await/async and Promise.then() syntax in a very confusing way. It is not an error to mix the two syntaxes, but I do not recommend it. Stick to just await/async
Void Return
Your action actually does not return any value right now because your inner then function doesn't return anything. The only return is inside of the then is in the x.map callback. await syncItems is the returned value for the mapper, not for your function.
Right now, here's what your thunk does:
open a connection
get items from realm
add a listener to those items which logs the changes
returns a Promise which resolves to void
Solution
I believe what you want is this:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
try {
const projectRealm = await Realm.open(config);
const syncItems = await projectRealm.objects("Item");
console.log(syncItems);
return syncItems;
} catch (error) {
console.log(error);
throw error;
}
}
);
Without the logging, it can be simplified to:
export const addItemsTest = createAsyncThunk(
"addItems",
async (config: any) => {
const projectRealm = await Realm.open(config);
return await projectRealm.objects("Item");
}
);
You don't need to catch errors because the createAsyncThunk will handle errors by dispatching an error action.
Edit: Listening To Changes
It seems that your intention is to sync your redux store with changes in your Realm collection. So you want to add a listener to the collection that calls dispatch with some action to process the changes.
Here I am assuming that this action takes an array with all of the items in your collection. Something like this:
const processItems = createAction("processItems", (items: Item[]) => ({
payload: items
}));
Replacing the entire array in your state is the easiest approach. It will lead to some unnecessary re-renders when you replace item objects with identical versions, but that's not a big deal.
Alternatively, you could pass specific properties of the changes such as insertions and handle them in your reducer on a case-by-case basis.
In order to add a listener that dispatches processItems, we need access to two variables: the realm config and the redux dispatch. You can do this in your component or by calling an "init" action. I don't think there's really much difference. You could do something in your reducer in response to the "init" action if you wanted.
Here's a function to add the listener. The Realm.Results object is "array-like" but not exactly an array so we use [...x] to cast it to an array.
FYI this function may throw errors. This is good if using in createAsyncThunk, but in a component we would want to catch those errors.
const loadCollection = async (config: Realm.Configuration, dispatch: Dispatch): Promise<void> => {
const projectRealm = await Realm.open(config);
const collection = await projectRealm.objects<Item>("Item");
collection.addListener((x, changes) => {
dispatch(processItems([...x]));
});
}
Adding the listener through an intermediate addListener action creator:
export const addListener = createAsyncThunk(
"init",
async (config: Realm.Configuration, { dispatch }) => {
return await loadCollection(config, dispatch);
}
);
// is config a prop or an imported global variable?
const InitComponent = ({config}: {config: Realm.Configuration}) => {
const dispatch = useDispatch();
useEffect( () => {
dispatch(addListener(config));
}, [config, dispatch]);
/* ... */
}
Adding the listener directly:
const EffectComponent = ({config}: {config: Realm.Configuration}) => {
const dispatch = useDispatch();
useEffect( () => {
// async action in a useEffect need to be defined and then called
const addListener = async () => {
try {
loadCollection(config, dispatch);
} catch (e) {
console.error(e);
}
}
addListener();
}, [config, dispatch]);
/* ... */
}

Calling another action after one is completed in React

I'm using Redux Thunk.
I got an async operation (updating message to db), and I want to wait for it to complete and then get the updated messages array from the db.
I tried:
const handleWriteMessage = async (e) => {
e.preventDefault()
await dispatch(
writeMessage({
sender: data.sender,
subject: data.subject,
receiver: data.receiver,
message: data.message,
created: date
})
)
dispatch(getMessages())
}
But it doesn't mind the await and runs getMessages() immediately when handleWriteMessage is called.
I tried to do it in the action itself after it's completed:
axios
.post('http://localhost:4000/api/messages/writeMessage', msg, config)
.then((res) => {
getMessages()
dispatch({
type: WRITE_MESSAGE_SUCCESS
})
})
But it's not working too.
What am I missing?
It seem that handleWriteMessage should not by async, it must return function that will accept dispatch and may execute async function, see redux-thunk docs.
See snippet below and its output.
var thunk = createThunkMiddleware();
var log = (state = [], action) => state.concat(action.message || action.type);
var store = Redux.createStore(log, [], Redux.applyMiddleware(thunk, logger));
(async() => {
store.dispatch(asyncAC('async message 1.a'))
.then(() => asyncAC('async message 1.b'))
.then(store.dispatch);
store.dispatch(syncAC('sync 1'));
await store.dispatch(asyncAC('await async message 2.a'));
store.dispatch(syncAC('sync 2'));
store.dispatch(asyncAC('await async message 3.a'))
.then(() => store.dispatch(asyncAC('then async message 3.b')));
})();
function syncAC(m) {
return {
type: 'log',
message: m
}
}
function asyncAC(m) {
return (dispatch) => {
return new Promise(resolve => setTimeout(resolve, 1000, syncAC(m)))
.then(dispatch);
}
}
// redux-thunk itself
function createThunkMiddleware(extraArgument) {
return function(_ref) {
var dispatch = _ref.dispatch,
getState = _ref.getState;
return function(next) {
return function(action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
};
};
}
// logger middleware
function logger({
getState
}) {
return next => action => {
console.log('will dispatch', action)
// Call the next dispatch method in the middleware chain.
const returnValue = next(action)
console.log('state after dispatch', getState())
// This will likely be the action itself, unless
// a middleware further in chain changed it.
return returnValue
}
}
<script src="https://unpkg.com/redux#4.0.5/dist/redux.js"></script>

Redux-logic subscription cancel for given subscriber

I am trying to implement subscriptions with redux-logic middleware.
The idea is following: when data is fetched from server, to call callback for each subscriber passing fetched data as arguments.
// logic/subscriptions.js
const fetchLatestLogic = createLogic({
type: FETCH_LATEST_DATA,
latest: true,
process({getState, action}, dispatch, done) {
const {seriesType, nextUpdateTime} = action.payload;
const callbacks = getState()[seriesType][nextUpdateTime].callbacks
apiFetch(seriesType)
.then(data => {
callbacks.forEach(callback => callback(seriesType, data));
done()
})
}
})
const subscribeLogic = createLogic({
type: SUBSCRIPTIONS_SUBSCRIBE,
cancelType: SUBSCRIPTIONS_REMOVE,
process({getState, action, cancelled$}, dispatch) {
const {seriesType, nextUpdateTime, updateInterval, subscriberId, callback} = action.payload;
const interval = setInterval(() => {
dispatch(fetchLatestData(seriesType, nextUpdateTime))
}, updateInterval);
cancelled$.subscribe(() => {
clearInterval(interval)
})
}
})
// reducers/subscriptions.js
import update from 'immutability-helper';
update.extend('$autoArray', (value, object) => (object ? update(object, value) : update([], value)));
const initialState = {
'SERIESTYPE1': {}
'SERIESTYPE2': {}
}
// state modifications using 'immutable-helpers'
const serieAddSubscriberForTime = (seriesSubscriptions, time, subscriber) =>
update(seriesSubscriptions, {
[time]: {
$autoArray: {
$push: [subscriber]
}
}
});
// state modifications using 'immutable-helpers'
const serieRemoveSubscriberForTime = (seriesSubscriptions, subscriptionTime, subscriber) => {
const subscriptions = seriesSubscriptions[subscriptionTime].filter(s => s.subscriberId !== subscriber.subscriberId);
if (subscriptions.length === 0) {
return update(seriesSubscriptions, { $unset: [subscriptionTime] });
}
return { ...seriesSubscriptions, ...{ [subscriptionTime]: subscriptions }
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case SUBSCRIPTIONS_SUBSCRIBE: {
const { seriesType, nextUpdateTime, subscriber} = action.payload;
const newSubscriptionAdded = serieAddSubscriberForTime(state[seriesType], nextUpdateTime, subscriber);
const oldSubscriptionRemoved = serieRemoveSubscriberForTime(state[seriesType], nextUpdateTime, subscriber);
return update(state, { [seriesType]: { ...oldSubscriptionRemoved, ...newSubscriptionAdded } });
}
default:
return state;
}
}
How would it be possible to cancel running interval for given subscriber only? *(Without dispatching intervalID to reducer and saving it in state?)
Because by just dispatching action
cancelType: SUBSCRIPTIONS_REMOVE
will remove all intervals for all subscriptions with my current implementation.
UPDATE: actually there is much more smooth way to do the cancellation logic.
cancelled$
is an observable, and the RxJS .subscribe() accepts three functions as arguments:
[onNext] (Function): Function to invoke for each element in the observable sequence.
[onError] (Function): Function to invoke upon exceptional termination of the observable sequence.
[onCompleted] (Function): Function to invoke upon graceful termination of the observable sequence.
so the argument of onNext function is an emited value, and since in our case its the SUBSCRIPTIONS_REMOVE action, we can access its payload and do the cancellation depending on that payload:
cancelled$.subscribe(cancellAction => {
if (cancellAction.payload.subscriberId === subscriberId &&
cancellAction.payload.seriesType === seriesType) {
clearTimeout(runningTimeout);
}
})

Why can't I dispatch an action when a promise resolves inside Redux middleware?

Background
I am writing a piece of Redux middleware that makes an axios request and uses Cheerio to parse the result.
Problem
When the Axios promise resolves and I try to dispatch a fulfilled action the action does not show up in the store's action log in the test.
Middleware
function createMiddleware() {
return ({ dispatch, getState }) => next => action => {
if (isCorrectAction(action)===true){
const pendingAction = {
type: `${action.type}_PENDING`,
payload: action.payload
}
dispatch(pendingAction)
axios.get(action.payload.url)
.then(response => {
let $ = cheerio.load(response.data)
let parsedData = action.payload.task($)
const fulfilledAction = {
type: `${action.type}_FULFILLED`,
payload: {
parsedData
}
}
dispatch(fulfilledAction) // dispatch that is problematic
})
.catch( err => {
});
}
return next(action);
}
}
Test that fulfilled action is dispatched fails
it('should dispatch ACTION_FULFILLED once', () => {
nock("http://www.example.com")
.filteringPath(function(path) {
return '/';
})
.get("/")
.reply(200, '<!doctype html><html><body><div>text</div></body></html>');
const expectedActionTypes = ['TASK', 'TASK_PENDING', 'TASK_FULFILLED']
// Initialize mockstore with empty state
const initialState = {}
const store = mockStore(initialState)
store.dispatch(defaultAction)
const actionsTypes = store.getActions().map(e => e.type)
expect(actionsTypes).has.members(expectedActionTypes);
expect(actionsTypes.length).equal(3);
});
Solution - Promise needs to be returned in the mocha test
The solution is to rewrite the mocha test so that the promise is returned. I mistakenly thought that by using nock to intercept the HTTP request that the promise would become synchronous.
The working test looks like:
it('should dispatch ACTION_FULFILLED once', () => {
nock("http://www.example.com")
.filteringPath(function(path) {
return '/';
})
.get("/")
.reply(200, '<!doctype html><html><body><div>text</div></body></html>');
const store = mockStore();
return store.dispatch(defaultScrapingAction)
.then(res => {
const actionsTypes = store.getActions().map(e => e.type)
expect(actionsTypes).has.members(expectedActionTypes);
expect(actionsTypes.length).equal(3);
})
});

Categories