如何使用 typescript 在 redux 工具包中重用 reducer?
How to reuse reducers in redux toolkit with typescript?
我有这样的一片:
const authSlice = createSlice({
name: "auth",
initialState: {
...initialUserInfo,
...initialBasicAsyncState,
},
reducers: {
setUser: (state, { payload }: PayloadAction<{ userObj: User }>) => {
const { id, email, googleId, facebookId } = payload.userObj;
state.id = id;
state.email = email;
if (googleId) state.googleId = googleId;
if (facebookId) state.facebookId = facebookId;
},
clearUser: (state) => {
state.id = "";
state.email = "";
state.googleId = "";
state.facebookId = "";
},
},
extraReducers: (builder) =>
builder
.addCase(getUser.pending, (state, action) => {
state.isLoading = true;
})
.addCase(getUser.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.errors = null;
})
.addCase(getUser.rejected, (state, action) => {
state.isLoading = false;
if (action.payload) {
state.errors = action.payload;
} else if (action.error) state.errors = action.error;
}),
});
我想为从 extraReducers
开始的部分编写一段可重用的代码,因为我将在多个片段中处理相同的异步请求。我阅读了文档,但仍然无法理解如何完成此操作。
interface CustomState {
isLoading: boolean
isSuccess: boolean
errors: ValidationErrors | SerializedError | null
}
function apiReducerBuilder<T, U>(
builder: ActionReducerMapBuilder<CustomState>,
customThunk: AsyncThunk<
T,
U,
{
rejectValue: ValidationErrors
}
>
) {
return builder
.addCase(customThunk.pending, (state) => {
state.isLoading = true
})
.addCase(customThunk.fulfilled, (state) => {
state.isLoading = false
state.isSuccess = true
state.errors = null
})
.addCase(customThunk.rejected, (state, action) => {
state.isLoading = false
if (action.payload) {
state.errors = action.payload
} else if (action.error) state.errors = action.error
})
}
用法:
extraReducers: (builder) => apiReducerBuilder(builder, getUser)
这仅在您传递相同的状态类型时才有效Custom State
。而且您的打字非常适合thunk。否则你必须在调用 apiReducerBuilder
时声明类型
因此,请确保您始终将相同的 Custom State
传递给您的构建器和 Thunk,否则它将无法工作。
编辑
也许,您可以使用自定义状态类型扩展您的切片状态类型,它可能会起作用。我没试过。所以,我会留给你看看它是否有效。
有一个 example in the docs 展示了如何使用 addMatcher
和类型谓词,如 action.type.endsWith('/pending')
来匹配任何待处理的操作。
我玩了一下,其中一个困难的事情是需要按特定顺序调用 builder
函数:addCase
,然后是 addMatcher
,然后 addDefaultCase
。所以我们不能先应用一堆addMatcher
调用,然后再正常使用builder
。
另一个困难的部分是了解 rejectWithValue
的 payload
类型,因为拒绝值不是 AsyncThunk
类型的泛型之一(至少不是直接的)。
我想到的最佳解决方案是使用单个 addMatcher
调用来处理所有三种 thunk 情况。
这些基本类型和类型保护是copied from the docs。
type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>
type PendingAction = ReturnType<GenericAsyncThunk['pending']>
type RejectedAction = ReturnType<GenericAsyncThunk['rejected']>
type FulfilledAction = ReturnType<GenericAsyncThunk['fulfilled']>
function isPendingAction(action: AnyAction): action is PendingAction {
return action.type.endsWith('/pending')
}
function isRejectedAction(action: AnyAction): action is RejectedAction {
return action.type.endsWith('/rejected')
}
function isFulfilledAction(action: AnyAction): action is FulfilledAction {
return action.type.endsWith('/fulfilled')
}
动作匹配器是一个 curried 函数,如果动作名称以任何这些 thunk 的 typePrefix
开头,它接受一对多 thunk 和 returns true
。
const isThunk = <T extends AsyncThunk<any, any, any>[]>(...thunks: T) =>
(action: AnyAction) =>
thunks.some((thunk) => action.type.startsWith(thunk.typePrefix));
case reducer 处理 thunk 的所有三种情况。它调用类型保护函数来确定 action
是哪种类型并相应地更新状态。我们要求 state
使用我们正在更新的属性扩展类型。
type BasicAsyncState = {
isLoading: boolean;
isSuccess: boolean;
errors: any;
};
const thunkHandler = <S extends BasicAsyncState>(
state: Draft<S>,
action: AnyAction
): void => {
if (isPendingAction(action)) {
state.isLoading = true;
} else if (isFulfilledAction(action)) {
state.isLoading = false;
state.isSuccess = true;
state.errors = null;
} else if (isRejectedAction(action)) {
state.isSuccess = false;
state.isLoading = false;
state.errors = action.error;
}
};
您可以在与其他构建器回调相同的块中使用这两个函数。请记住 addMatcher
总是需要在 addCase
之后。
// can match one thunk
.addMatcher(isThunk(getUser), thunkHandler)
// or multiple thunks
.addMatcher(isThunk(getUser, loadSomething), thunkHandler)
我有这样的一片:
const authSlice = createSlice({
name: "auth",
initialState: {
...initialUserInfo,
...initialBasicAsyncState,
},
reducers: {
setUser: (state, { payload }: PayloadAction<{ userObj: User }>) => {
const { id, email, googleId, facebookId } = payload.userObj;
state.id = id;
state.email = email;
if (googleId) state.googleId = googleId;
if (facebookId) state.facebookId = facebookId;
},
clearUser: (state) => {
state.id = "";
state.email = "";
state.googleId = "";
state.facebookId = "";
},
},
extraReducers: (builder) =>
builder
.addCase(getUser.pending, (state, action) => {
state.isLoading = true;
})
.addCase(getUser.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.errors = null;
})
.addCase(getUser.rejected, (state, action) => {
state.isLoading = false;
if (action.payload) {
state.errors = action.payload;
} else if (action.error) state.errors = action.error;
}),
});
我想为从 extraReducers
开始的部分编写一段可重用的代码,因为我将在多个片段中处理相同的异步请求。我阅读了文档,但仍然无法理解如何完成此操作。
interface CustomState {
isLoading: boolean
isSuccess: boolean
errors: ValidationErrors | SerializedError | null
}
function apiReducerBuilder<T, U>(
builder: ActionReducerMapBuilder<CustomState>,
customThunk: AsyncThunk<
T,
U,
{
rejectValue: ValidationErrors
}
>
) {
return builder
.addCase(customThunk.pending, (state) => {
state.isLoading = true
})
.addCase(customThunk.fulfilled, (state) => {
state.isLoading = false
state.isSuccess = true
state.errors = null
})
.addCase(customThunk.rejected, (state, action) => {
state.isLoading = false
if (action.payload) {
state.errors = action.payload
} else if (action.error) state.errors = action.error
})
}
用法:
extraReducers: (builder) => apiReducerBuilder(builder, getUser)
这仅在您传递相同的状态类型时才有效Custom State
。而且您的打字非常适合thunk。否则你必须在调用 apiReducerBuilder
因此,请确保您始终将相同的 Custom State
传递给您的构建器和 Thunk,否则它将无法工作。
编辑
也许,您可以使用自定义状态类型扩展您的切片状态类型,它可能会起作用。我没试过。所以,我会留给你看看它是否有效。
有一个 example in the docs 展示了如何使用 addMatcher
和类型谓词,如 action.type.endsWith('/pending')
来匹配任何待处理的操作。
我玩了一下,其中一个困难的事情是需要按特定顺序调用 builder
函数:addCase
,然后是 addMatcher
,然后 addDefaultCase
。所以我们不能先应用一堆addMatcher
调用,然后再正常使用builder
。
另一个困难的部分是了解 rejectWithValue
的 payload
类型,因为拒绝值不是 AsyncThunk
类型的泛型之一(至少不是直接的)。
我想到的最佳解决方案是使用单个 addMatcher
调用来处理所有三种 thunk 情况。
这些基本类型和类型保护是copied from the docs。
type GenericAsyncThunk = AsyncThunk<unknown, unknown, any>
type PendingAction = ReturnType<GenericAsyncThunk['pending']>
type RejectedAction = ReturnType<GenericAsyncThunk['rejected']>
type FulfilledAction = ReturnType<GenericAsyncThunk['fulfilled']>
function isPendingAction(action: AnyAction): action is PendingAction {
return action.type.endsWith('/pending')
}
function isRejectedAction(action: AnyAction): action is RejectedAction {
return action.type.endsWith('/rejected')
}
function isFulfilledAction(action: AnyAction): action is FulfilledAction {
return action.type.endsWith('/fulfilled')
}
动作匹配器是一个 curried 函数,如果动作名称以任何这些 thunk 的 typePrefix
开头,它接受一对多 thunk 和 returns true
。
const isThunk = <T extends AsyncThunk<any, any, any>[]>(...thunks: T) =>
(action: AnyAction) =>
thunks.some((thunk) => action.type.startsWith(thunk.typePrefix));
case reducer 处理 thunk 的所有三种情况。它调用类型保护函数来确定 action
是哪种类型并相应地更新状态。我们要求 state
使用我们正在更新的属性扩展类型。
type BasicAsyncState = {
isLoading: boolean;
isSuccess: boolean;
errors: any;
};
const thunkHandler = <S extends BasicAsyncState>(
state: Draft<S>,
action: AnyAction
): void => {
if (isPendingAction(action)) {
state.isLoading = true;
} else if (isFulfilledAction(action)) {
state.isLoading = false;
state.isSuccess = true;
state.errors = null;
} else if (isRejectedAction(action)) {
state.isSuccess = false;
state.isLoading = false;
state.errors = action.error;
}
};
您可以在与其他构建器回调相同的块中使用这两个函数。请记住 addMatcher
总是需要在 addCase
之后。
// can match one thunk
.addMatcher(isThunk(getUser), thunkHandler)
// or multiple thunks
.addMatcher(isThunk(getUser, loadSomething), thunkHandler)