你好吗?
我在 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()}
你能帮我吗?
提前致谢:)
如果没有最小的可重现示例,很难说。但我想说,每次获取后过滤器引用很可能会以某种方式发生变化,因此会无休止地执行。
useEffect(() => {
if (refetch) refetch();
}, [filters]);
请记住,redux 状态是不可变的,每次更新后都会返回一个全新的状态,无论它有多小。因此,如果您更改过滤器状态中的任何内容,useEffect 将被重新触发。 要检查是否属于这种情况,请尝试从依赖项数组中删除过滤器。 如果这不起作用,请尝试创建一个最小的、可重现的示例