谁在 React JS with typescript 和 React saga 中从 API 加载下拉选项?
Who to load dropdown options from API in react JS with typescript and react saga?
这是我的 page,我想从 API 加载品牌选项。
我在下面附上了传奇故事:
Action.tsx
export const getBrandsForDropdown = (request: IPagination) => {
return {
type: actions,
payload: request
}
}
Api.tsx
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
console.log("get brand drop down");
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null }).then(
(data) => {
console.log("get brand drop down in ");
return { data, error: null };
},
(error) => {
return { data: null, error };
}
);
};
Reducer.ts
case actions.GET_BRANDS_DROPDOWN_PENDING:
return {
...state,
loading: true,
};
case actions.GET_BRANDS_DROPDOWN_REJECTED:
return {
...state,
loading: false,
};
case actions.GET_BRANDS_DROPDOWN_RESOLVED:
return {
...state,
loading: false,
brandOptions: action.payload,
};
Saga.ts
function* getBrandForDropDownSaga(action: HandleGetBrandsForDropdown) {
yield put(switchGlobalLoader(true));
yield put(pendingViewBrand());
try {
const { data } = yield getBrandsForDropdown();
yield put(resolvedViewBrand(data));
yield put(switchGlobalLoader(false));
} catch (error) {
yield put(switchGlobalLoader(false));
return;
}
}
在此之后我不知道如何在我的页面中调用它并将其作为品牌下拉列表中的选项
原始答案:只需使用 Thunk
您可以使用 redux-saga
执行此操作,但我不推荐这样做。 redux-thunk
更容易使用。 Thunk 也内置于 @reduxjs/toolkit
中,这使得它更容易。
不需要 IPagination
参数,因为您总是将分页设置为 {page: 1, limit: 1000}
尝试这样的事情:
import {
createAsyncThunk,
createSlice,
SerializedError
} from "@reduxjs/toolkit";
import { IDropdownOption } from "office-ui-fabric-react";
import client from ???
// thunk action creator
export const fetchBrandsForDropdown = createAsyncThunk(
"fetchBrandsForDropdown",
async (): Promise<IDropdownOption[]> => {
const query = `user/master/brands?page=1&limit=1000`;
return client(query, { body: null });
// don't catch errors here, let them be thrown
}
);
interface State {
brandOptions: {
data: IDropdownOption[];
error: null | SerializedError;
};
// can have other properties
}
const initialState: State = {
brandOptions: {
data: [],
error: null
}
};
const slice = createSlice({
name: "someName",
initialState,
reducers: {
// could add any other case reducers here
},
extraReducers: (builder) =>
builder
// handle the response from your API by updating the state
.addCase(fetchBrandsForDropdown.fulfilled, (state, action) => {
state.brandOptions.data = action.payload;
state.brandOptions.error = null;
})
// handle errors
.addCase(fetchBrandsForDropdown.rejected, (state, action) => {
state.brandOptions.error = action.error;
})
});
export default slice.reducer;
在您的组件中,终止 brandOptions
状态并从 Redux 访问它。使用 useEffect
.
加载组件时加载选项
const brandOptions = useSelector((state) => state.brandOptions.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchBrandsForDropdown());
}, [dispatch]);
更新:与 Saga
关于如何编写 saga 的总体思路在您的代码中是正确的。
take
父异步操作。
put
一个未决操作。
call
API 获取数据。
put
带有数据的已解决操作或带有错误的已拒绝操作。
我在您的故事中看到的最大错误是:
- 在上游捕获错误。
- 数据类型不匹配。
- 未将 API 函数包装在
call
效果中。
错误处理
您的 brands.api
函数都在捕获它们的 API 错误,这意味着 Promise
将始终得到解决。您的 saga 中的 try
/catch
不会有要捕获的错误。
如果你想 catch
传奇中的错误,那么你需要从函数 getBrandsForDropdown
等中删除 catch
。你可以 return 数据直接而不是映射到 { result: data, error: null }
。所以删除整个 then
函数。我推荐这种方法。
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null });
}
如果你想从所有 API 调用中保留 returning 一个 { result, error }
对象的当前结构,那么你需要修改 saga 以查找函数中的错误return.
function* getBrandForDropDownSaga() {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
const { data, error } = yield call(getBrandsForDropdown);
if (error) {
yield put(rejectedGetBrands(error.message));
} else {
yield put(resolvedGetBrands(data));
}
yield put(switchGlobalLoader(false));
}
不匹配的数据类型
您的减速器和状态中存在某种类型不匹配,您需要解决。在某些地方,您使用的是数组 IBrand[]
,而在其他地方,您使用的是对象 { results: IBrand[]; totalItems: number; currentPage: string; }
。如果您将 return 类型 IState
添加到减速器,那么您会看到。
单个 IBrand
和数组之间也存在不匹配。我不知道您 API 响应的确切形状,但 getBrandsForDropdown
肯定有 array
品牌。您的 saga getBrandForDropDownSaga
正在调度 resolvedViewBrand(data)
,它采用单个 IBrand
而不是 resolvedGetBrands(data)
,它采用数组 IBrand[]
。如果您将 return 类型添加到 brands.api
文件中的函数,那么您会看到这些错误由 Typescript 突出显示。
不要重复自己
您可以在 API 和 getBrands
与 getBrandsForDropdown
之间的 saga 中进行大量组合。获取下拉列表的品牌只是 getBrands
的一个特例,您可以在其中设置某些参数:{ page: 1, limit: 1000 }
.
export interface IPagination {
page?: number;
limit?: number;
sort?: "ASC" | "DESC";
column?: string;
}
export const getBrands = async (request: IPagination): Promise<IBrands> => {
const res = await axios.get<IBrands>('/user/master/brands', {
params: request,
});
return res.data;
};
function* coreGetBrandsSaga(request: IPagination) {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
try {
const data = yield call(getBrands, request);
yield put(resolvedGetBrands(data));
} catch (error) {
yield put(rejectedGetBrands(error?.message));
}
yield put(switchGlobalLoader(false));
}
function* getBrandsSaga(action: HandleGetBrands) {
const { sort } = action.payload;
if ( sort ) {
yield put(setSortBrands(sort));
// what about column?
}
const brandsState = yield select((state: AppState) => state.brands);
const request = {
// defaults
page: 1,
limit: brandsState.rowsPerPage,
column: brandsState.column,
// override with action
...action.payload,
}
// the general function can handle the rest
yield coreGetBrandsSaga(request);
}
function* getBrandsForDropDownSaga() {
// handle by the general function, but set certain the request arguments
yield coreGetBrandsSaga({
page: 1,
limit: 1000,
sort: "ASC",
column: "name",
})
}
export default function* brandsSaga() {
yield takeLatest(HANDLE_GET_BRANDS, getBrandsSaga);
yield takeLatest(GET_BRANDS_DROPDOWN, getBrandForDropDownSaga);
...
}
这是我的 page,我想从 API 加载品牌选项。 我在下面附上了传奇故事:
Action.tsx
export const getBrandsForDropdown = (request: IPagination) => {
return {
type: actions,
payload: request
}
}
Api.tsx
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
console.log("get brand drop down");
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null }).then(
(data) => {
console.log("get brand drop down in ");
return { data, error: null };
},
(error) => {
return { data: null, error };
}
);
};
Reducer.ts
case actions.GET_BRANDS_DROPDOWN_PENDING:
return {
...state,
loading: true,
};
case actions.GET_BRANDS_DROPDOWN_REJECTED:
return {
...state,
loading: false,
};
case actions.GET_BRANDS_DROPDOWN_RESOLVED:
return {
...state,
loading: false,
brandOptions: action.payload,
};
Saga.ts
function* getBrandForDropDownSaga(action: HandleGetBrandsForDropdown) {
yield put(switchGlobalLoader(true));
yield put(pendingViewBrand());
try {
const { data } = yield getBrandsForDropdown();
yield put(resolvedViewBrand(data));
yield put(switchGlobalLoader(false));
} catch (error) {
yield put(switchGlobalLoader(false));
return;
}
}
在此之后我不知道如何在我的页面中调用它并将其作为品牌下拉列表中的选项
原始答案:只需使用 Thunk
您可以使用 redux-saga
执行此操作,但我不推荐这样做。 redux-thunk
更容易使用。 Thunk 也内置于 @reduxjs/toolkit
中,这使得它更容易。
不需要 IPagination
参数,因为您总是将分页设置为 {page: 1, limit: 1000}
尝试这样的事情:
import {
createAsyncThunk,
createSlice,
SerializedError
} from "@reduxjs/toolkit";
import { IDropdownOption } from "office-ui-fabric-react";
import client from ???
// thunk action creator
export const fetchBrandsForDropdown = createAsyncThunk(
"fetchBrandsForDropdown",
async (): Promise<IDropdownOption[]> => {
const query = `user/master/brands?page=1&limit=1000`;
return client(query, { body: null });
// don't catch errors here, let them be thrown
}
);
interface State {
brandOptions: {
data: IDropdownOption[];
error: null | SerializedError;
};
// can have other properties
}
const initialState: State = {
brandOptions: {
data: [],
error: null
}
};
const slice = createSlice({
name: "someName",
initialState,
reducers: {
// could add any other case reducers here
},
extraReducers: (builder) =>
builder
// handle the response from your API by updating the state
.addCase(fetchBrandsForDropdown.fulfilled, (state, action) => {
state.brandOptions.data = action.payload;
state.brandOptions.error = null;
})
// handle errors
.addCase(fetchBrandsForDropdown.rejected, (state, action) => {
state.brandOptions.error = action.error;
})
});
export default slice.reducer;
在您的组件中,终止 brandOptions
状态并从 Redux 访问它。使用 useEffect
.
const brandOptions = useSelector((state) => state.brandOptions.data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchBrandsForDropdown());
}, [dispatch]);
更新:与 Saga
关于如何编写 saga 的总体思路在您的代码中是正确的。
take
父异步操作。put
一个未决操作。call
API 获取数据。put
带有数据的已解决操作或带有错误的已拒绝操作。
我在您的故事中看到的最大错误是:
- 在上游捕获错误。
- 数据类型不匹配。
- 未将 API 函数包装在
call
效果中。
错误处理
您的 brands.api
函数都在捕获它们的 API 错误,这意味着 Promise
将始终得到解决。您的 saga 中的 try
/catch
不会有要捕获的错误。
如果你想 catch
传奇中的错误,那么你需要从函数 getBrandsForDropdown
等中删除 catch
。你可以 return 数据直接而不是映射到 { result: data, error: null }
。所以删除整个 then
函数。我推荐这种方法。
export const getBrandsForDropdown = async () => {
const page = 1;
const limit = 1000;
const query = `user/master/brands?page=${page}&limit=${limit}`;
return client(query, { body: null });
}
如果你想从所有 API 调用中保留 returning 一个 { result, error }
对象的当前结构,那么你需要修改 saga 以查找函数中的错误return.
function* getBrandForDropDownSaga() {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
const { data, error } = yield call(getBrandsForDropdown);
if (error) {
yield put(rejectedGetBrands(error.message));
} else {
yield put(resolvedGetBrands(data));
}
yield put(switchGlobalLoader(false));
}
不匹配的数据类型
您的减速器和状态中存在某种类型不匹配,您需要解决。在某些地方,您使用的是数组 IBrand[]
,而在其他地方,您使用的是对象 { results: IBrand[]; totalItems: number; currentPage: string; }
。如果您将 return 类型 IState
添加到减速器,那么您会看到。
单个 IBrand
和数组之间也存在不匹配。我不知道您 API 响应的确切形状,但 getBrandsForDropdown
肯定有 array
品牌。您的 saga getBrandForDropDownSaga
正在调度 resolvedViewBrand(data)
,它采用单个 IBrand
而不是 resolvedGetBrands(data)
,它采用数组 IBrand[]
。如果您将 return 类型添加到 brands.api
文件中的函数,那么您会看到这些错误由 Typescript 突出显示。
不要重复自己
您可以在 API 和 getBrands
与 getBrandsForDropdown
之间的 saga 中进行大量组合。获取下拉列表的品牌只是 getBrands
的一个特例,您可以在其中设置某些参数:{ page: 1, limit: 1000 }
.
export interface IPagination {
page?: number;
limit?: number;
sort?: "ASC" | "DESC";
column?: string;
}
export const getBrands = async (request: IPagination): Promise<IBrands> => {
const res = await axios.get<IBrands>('/user/master/brands', {
params: request,
});
return res.data;
};
function* coreGetBrandsSaga(request: IPagination) {
yield put(switchGlobalLoader(true));
yield put(pendingGetBrands());
try {
const data = yield call(getBrands, request);
yield put(resolvedGetBrands(data));
} catch (error) {
yield put(rejectedGetBrands(error?.message));
}
yield put(switchGlobalLoader(false));
}
function* getBrandsSaga(action: HandleGetBrands) {
const { sort } = action.payload;
if ( sort ) {
yield put(setSortBrands(sort));
// what about column?
}
const brandsState = yield select((state: AppState) => state.brands);
const request = {
// defaults
page: 1,
limit: brandsState.rowsPerPage,
column: brandsState.column,
// override with action
...action.payload,
}
// the general function can handle the rest
yield coreGetBrandsSaga(request);
}
function* getBrandsForDropDownSaga() {
// handle by the general function, but set certain the request arguments
yield coreGetBrandsSaga({
page: 1,
limit: 1000,
sort: "ASC",
column: "name",
})
}
export default function* brandsSaga() {
yield takeLatest(HANDLE_GET_BRANDS, getBrandsSaga);
yield takeLatest(GET_BRANDS_DROPDOWN, getBrandForDropDownSaga);
...
}