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?
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
}