React-redux thunk 导致奇怪的循环行为?

问题描述 投票:0回答:1

你好吗?

我在 React 应用程序中使用 Redux 时遇到问题。当从给定组件内的 useEffect 中分派特定的 thunk 操作时,即使依赖项数组为空,也会出现一些奇怪的循环行为。我已将此操作分派到代码中的其他位置,并且工作得很好。

我有一个 pointSlice,它有一个名为 fetchPoints 的异步操作 thunk。基本上,调用一个利用 axios 实例进行 API 调用的方法:

import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { getPointsService } from "../../services/point/getPointForTeacher";

interface PointState {
  statusCode?: number;
  errors?: Array<any>;
  error?: boolean;
  data: Array<any>;
  status: "idle" | "loading" | "succeeded" | "failed";
}

const initialState: PointState = {
  data: [],
  status: "idle",
  error: false,
};

export const fetchPoints = createAsyncThunk(
  "point/fetchPoints",
  async (
    {
      id,
      month = new Date().getMonth() + 1,
      year = new Date().getFullYear(),
    }: fetchPointsProps,
    { rejectWithValue }
  ) => {
    try {
      const { data } = await getPointsService({ id, month, year });

      return data;
    } catch (err: any) {
      return rejectWithValue(err.response.data);
    }
  }
);

const pointSlice = createSlice({
  name: "point",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchPoints.pending, (state) => {
        state.status = "loading";
      })
      .addCase(
        fetchPoints.fulfilled,
        (state, action: PayloadAction<any>) => {
          state.status = "succeeded";
          state.data = action.payload;
        }
      )
      .addCase(
        fetchPoints.rejected,
        (state, action: PayloadAction<any>) => {
          state.status = "failed";
        }
      );
  },
});

export default pointSlice.reducer;

我有我的filterSlice:

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

const initialState = {
  month: localStorage.getItem("filterYear") || new Date().getMonth() + 1,
  year: localStorage.getItem("filterYear") || new Date().getFullYear(),
  userId: "",
};

const filterSlice = createSlice({
  name: "filters",
  initialState,
  reducers: {
    setYearFilter(state, action: PayloadAction<number | string>) {
      state.year = action.payload;
    },
    setMonthFilter(state, action: PayloadAction<number | string>) {
      state.month = action.payload;
    },
    setUserIdFilter(state, action: PayloadAction<string>) {
      state.userId = action.payload;
    },
    clearFilters(state) {
      state.month = new Date().getMonth() + 1;
      state.year = new Date().getFullYear();
      state.userId = "";
      localStorage.removeItem("filterYear");
      localStorage.removeItem("filterMonth");
    },
    saveFilters(state) {
      localStorage.setItem("filterYear", state.year.toString());
      localStorage.setItem("filterMonth", state.month.toString());
    },
  },
});

export const {
  setYearFilter,
  setMonthFilter,
  setUserIdFilter,
  clearFilters,
  saveFilters,
} = filterSlice.actions;

export default filterSlice.reducer;

我的商店配置(修剪导入):

const rootReducer = combineReducers({
  themeConfig: themeConfigSlice,
  point: pointSlice,
  filters: filtersSlice,
});

export const store = configureStore({
  reducer: rootReducer,
});

export type IRootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

不要太在意 themeConfig,它来自我用于这个项目的模板,称为 Vristo

好的,所以在我的 PointComponent 中,我在带有空数组的 useEffect 中调度 fetchPoints。当 state.point.status 为“正在加载”时,我返回我的 Loader 组件,否则返回我的 CustomDataTable 组件,它是来自 Vristo 的 AltPagination 的修改副本。
我正在注入我的数据和其他道具,但最重要的是我正在注入 fetch 方法作为 refetch。

const Point = () => {
  const dispatch = useDispatch<AppDispatch>();

  const { data, status, error } = useSelector(
    (state: IRootState) => state.point
  );

  const { userId, month, year } = useSelector(
    (state: IRootState) => state.filters
  );

  const fetch = () =>
    dispatch(
      fetchPoints({ id: "66a3fcc6f290e87b76d99795", month, year })
    );

  useEffect(() => {
    fetch();
  }, []);

  return status === "loading" ? (
    <CustomLoader />
  ) : (
    <CustomDataTable
      pageTitle="Title"
      data={data}
      searchableItens={["date"]}
      refetch={fetch}
      columns={[...]}
      filterByMonth
      filterByYear
    />
  );
};

export default Point;

这是我的 CustomDataTable 的结构(修剪了一些块):

const CustomDataTable = ({
  data,
  columns,
  searchableItens,
  pageTitle,
  filterByYear = false,
  filterByMonth = false,
  onRowClick,
  refetch,
}: ICustomDataTableProps) => {
  const dispatch = useDispatch<AppDispatch>();

  useEffect(() => {
    dispatch(setPageTitle(pageTitle));
  });

  const [page, setPage] = useState(1);
  const PAGE_SIZES = [10, 20, 30, 50, 100];
  const [pageSize, setPageSize] = useState(PAGE_SIZES[0]);
  const [initialRecords, setInitialRecords] = useState(sortBy(data, "id"));
  const [recordsData, setRecordsData] = useState(initialRecords);

  const [search, setSearch] = useState("");
  const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
    columnAccessor: "id",
    direction: "asc",
  });

  const currentDate = new Date();
  const currentYear = currentDate.getFullYear();
  // const currentMonth = currentDate.getMonth() + 1;

  const yearsToFilter = Array.from({ length: 5 }, (_, i) => currentYear - i);

  // const [monthToFilter, setMonthToFilter] = useState(currentYear);
  // const [yearToFilter, setYearToFilter] = useState(currentMonth);

  const filters = useSelector((state: IRootState) => state.filters);

  useEffect(() => {
    setPage(1);
  }, [pageSize]);

  useEffect(() => {
    const from = (page - 1) * pageSize;
    const to = from + pageSize;
    setRecordsData([...initialRecords.slice(from, to)]);
  }, [page, pageSize, initialRecords]);

  useEffect(() => {
    setInitialRecords(() => {
      if (data?.length > 0)
        return data?.filter((item) => {
          if (searchableItens) {
            let found = false;
            for (let searchable of searchableItens) {
              if (
                item?.[searchable]
                  ?.toString()
                  ?.toLowerCase()
                  ?.includes(search.toLowerCase())
              )
                found = true;
            }

            return found;
          }
        });

      return [];
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [search]);

  useEffect(() => {
    const data = sortBy(initialRecords, sortStatus.columnAccessor);
    if (data?.length > 0)
      setInitialRecords(
        sortStatus.direction === "desc" ? data.reverse() : data
      );
    setPage(1);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sortStatus]);

  useEffect(() => {
    if (refetch) refetch();
  }, [filters]);

  return (
    <div>
      <div className="panel mt-6">
        <div className="flex md:items-center md:flex-row flex-col mb-5 gap-5">
          <h5 className="font-semibold text-lg dark:text-white-light">
            {pageTitle}
          </h5>
        </div>

        <div className="ltr:ml-auto rtl:mr-auto panel mt-6 gap-6 flex">
          {searchableItens && (
            <input
              type="text"
              className="form-input w-auto"
              placeholder="Pesquisar..."
              value={search}
              onChange={(e) => setSearch(e.target.value)}
            />
          )}

          {filterByMonth && (
            <select
              className="form-input w-auto"
              onChange={(e) =>
                dispatch(setMonthFilter(parseInt(e.target.value)))
              }
            >
              <option value="">Month</option>
              {...monthOptions}
            </select>
          )}

          {filterByYear && (
            <select
              className="form-input w-auto"
              onChange={(e) =>
                dispatch(setYearFilter(parseInt(e.target.value)))
              }
            >
              <option value="">Year</option>
              {yearsToFilter.map((year) => (
                <option key={year} value={year}>
                  {year}
                </option>
              ))}
            </select>
          )}
        </div>

        <div className="datatables">
          <DataTable
            records={recordsData}
            columns={columns}
            totalRecords={initialRecords.length}
            recordsPerPage={pageSize}
            page={page}
            onPageChange={(p) => setPage(p)}
            recordsPerPageOptions={PAGE_SIZES}
            onRecordsPerPageChange={setPageSize}
            sortStatus={sortStatus}
            onSortStatusChange={setSortStatus}
            onRowClick={onRowClick}
          />
        </div>
      </div>
    </div>
  );
};

export default CustomDataTable;

底线,这里的想法是什么?这个想法是: 获取点,注入我的数据表中; 如果用户更改 CustomDataTable 中选择的年份或月份选项,则向过滤器发送更新; 更新过滤器后,启动我的 useEffect; 我们有重新获取方法吗?如果是这样,请调用它;

refetch 方法只是 fetchPoints 的调度,与第一步相同,在我的 Point 组件中使用。

事实是,这并没有按预期工作。 它递归地进行 API 调用。但同样,仅用于重新获取。 我什至不必更改年份或月份。它立即陷入加载/调用 fetchPoints 的循环中。

目前,这是我迄今为止尝试过的: 使用空依赖项数组设置我的 useEffect。应该只运行一次,对吗?是的,但是没有。给了我同样的行为。就好像该函数正在调用自己一样。 我也尝试使用 useCallback 代替。 不是直接从 select 中的 onChange 事件分派我的操作,而是利用 useState 并将它们输入到依赖项数组中。 而不是 refetch={fetch},refetch={() => fetch()}

你能帮我吗?

提前致谢:)

javascript reactjs debugging redux react-redux
1个回答
0
投票

如果没有最小的可重现示例,很难说。但我想说,每次获取后过滤器引用很可能会以某种方式发生变化,因此会无休止地执行。

  useEffect(() => {
    if (refetch) refetch();
  }, [filters]);

请记住,redux 状态是不可变的,每次更新后都会返回一个全新的状态,无论它有多小。因此,如果您更改过滤器状态中的任何内容,useEffect 将被重新触发。 要检查是否属于这种情况,请尝试从依赖项数组中删除过滤器。 如果这不起作用,请尝试创建一个最小的、可重现的示例

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.