谁在 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]);

CodeSandbox Link

更新:与 Saga

关于如何编写 saga 的总体思路在您的代码中是正确的。

  1. take 父异步操作。
  2. put 一个未决操作。
  3. call API 获取数据。
  4. 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 和 getBrandsgetBrandsForDropdown 之间的 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);
  ...
}

CodeSandbox