Reputation: 8336
Somehow I didn't find working example for the most basic case. I took example e.g. from here.
State:
interface AppUserState {
userId: string | undefined;
user: User;
status: 'idle' | 'pending' | 'succeeded' | 'failed';
error: Error | null;
}
I have a thunk:
export const fetchUser = createAsyncThunk(
'app-user/fetch-user',
async (id: string) => {
const response = await fetch(`/api/v1/user/id/${id}`);
return await response.json();
}
);
And a slice:
const usersSlice = createSlice({
name: 'app-user',
initialState,
reducers: {
setUserId(state, action: PayloadAction<string | undefined>) {
state.userId = action.payload;
}
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending.type, (state) => {
state.status = 'pending';
})
.addCase(fetchUser.fulfilled.type, (state, action: PayloadAction<any>) => {
if (state.status === 'pending') {
state.status = 'succeeded';
state.user = {
id: action.payload.id,
name: action.payload.name,
type: action.payload.Type
};
}
})
.addCase(fetchUser.rejected.type, (state, action: PayloadAction<any>) => {
if (state.status === 'pending') {
state.status = 'failed';
state.error = action.payload;
}
})
}
});
This doesn't work. When there's no user and fetch returns 404 rejected case is not handled.
FIX:
If I change the fetch to code below the rejected case is handled but the payload doesn't contain the error I pass to reject.
export const fetchUser = createAsyncThunk(
'app-user/fetch-user',
async (id: string) => {
const response = await fetch(`/api/v1/user/id/${id}`);
if (!response.ok) {
return Promise.reject(new Error(response.statusText));
}
return await response.json();
}
);
Instead the error is directly in action. This can be fixed by changing the rejected case:
.addCase(fetchUser.rejected.type, (state, action) => {
if (state.status === 'pending') {
state.status = 'failed';
if ('error' in action) {
state.error = action.error as Error;
}
}
})
But this seems not to be the standard way to do this. There's lots of examples that are not for fetch API. So how this should be done?
Upvotes: 1
Views: 248
Reputation: 202721
fetch
does not reject on status 404. See Checking that the fetch was successful for details.
It would seem you are about halfway there to processing the fetched data. I would suggest just surrounding all the asynchronous code/logic in a try/catch
, assume the "happy path", and if there are any errors/rejections along the way then return the error value with rejectWithValue
instead of throwing another error or returning a Promise.reject
. This places the "error" (whatever it is) on the *.rejected
action's payload
.
Example:
export const fetchUser = createAsyncThunk(
'app-user/fetch-user',
async (id: string, { thunkApi }) => {
try {
const response = await fetch(`/api/v1/user/id/${id}`);
if (!response.ok) {
return thunkApi.rejectWithValue(new Error(response.statusText));
}
return response.json();
} catch(error) {
return thunkApi.rejectWithValue(error);
}
}
);
const usersSlice = createSlice({
name: 'app-user',
initialState,
reducers: {
setUserId(state, action: PayloadAction<string | undefined>) {
state.userId = action.payload;
}
},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.status = 'pending';
})
.addCase(fetchUser.fulfilled, (state, action: PayloadAction<any>) => {
if (state.status === 'pending') {
state.status = 'succeeded';
state.user = {
id: action.payload.id,
name: action.payload.name,
type: action.payload.Type
};
state.error = null; // <-- clear any errors on success
}
})
.addCase(fetchUser.rejected, (state, action: PayloadAction<any>) => {
if (state.status === 'pending') {
state.status = 'failed';
state.error = action.payload; // *
}
})
}
});
Note: that you may need to adjust the AppUserState
interface to match anything you are rejecting in the thunk and setting into state.*
interface AppUserState {
userId?: string;
user: User;
status: 'idle' | 'pending' | 'succeeded' | 'failed';
error: Error | null; // <-- tweak this to match the code used
}
Upvotes: 1