如何配置React useEffect实现多依赖数据请求并避免重复接口调用?
问题根源
现有代码中多个useEffect存在联动触发逻辑,是重复请求的核心原因:
- 首次加载时,监听
currentPage的useEffect会触发1次fetchPosts,同时监听posts+currentPage的useEffect检测到初始posts为空,会再触发1次fetchPosts currentTimeline变更时,重置posts和currentPage的操作会依次触发另外两个useEffect,导致多次重复请求- 禁用
react-hooks/exhaustive-deps规则还导致了闭包陷阱,fetchPosts获取的永远是首次渲染的posts值,后续拼接数据会出现异常
解决方案
合并冗余逻辑,仅保留必要的副作用触发规则,完整修改后的代码如下:
// @flow import style from "./style.module.scss"; import React, { useState, useEffect, Suspense, useCallback } from "react"; import { Grid, NoSsr } from "@material-ui/core"; import { Post as PostComponent } from "@components"; import type { Post } from "@types"; import { getPosts, addPosts } from "../../api/Posts"; import { TimelineFooter, PostCreation } from "@components"; import { Box } from "@material-ui/core"; import InfiniteScroll from "react-infinite-scroll-component"; import Typography from "@material-ui/core/Typography"; import { timelineState } from "@atoms"; import { useRecoilValue } from "recoil"; import { deletePost } from "../../api/Posts"; function Timeline() { const [open, setOpen] = useState(false); const [posts, setPosts] = useState([]); const [hasMore, setHasMore] = useState(true); const [currentPage, setCurrentPage] = useState(0); const currentTimeline = useRecoilValue(timelineState); // 用useCallback + 函数式更新posts解决闭包问题 const fetchPosts = useCallback(() => { getPosts(currentTimeline.tab, currentPage).then((result: *) => { if (result.length > 0) { // 函数式更新,不依赖外层posts状态 setPosts(prevPosts => prevPosts.concat(result)); setHasMore(true); } else { setHasMore(false); } }); }, [currentTimeline.tab, currentPage]); // currentTimeline变更时重置所有状态 useEffect(() => { setPosts([]); setHasMore(true); setCurrentPage(0); }, [currentTimeline]); // 仅在currentPage变更时触发请求,覆盖三个场景: // 1. 首次加载currentPage初始为0触发 // 2. 翻页时currentPage自增触发 // 3. currentTimeline变更时重置currentPage为0触发 useEffect(() => { fetchPosts(); }, [fetchPosts]); const fetchMoreData = () => { setCurrentPage(prevPage => prevPage + 1); }; const handleClickOpen = () => { setOpen(true); }; const handleClose = () => { setOpen(false); }; const handlePostCreation = (message: string, files: Array<*>) => { let formData = new FormData(); formData.append("content", message); if (files) { for (const file of files) { formData.append("files", file); } } formData.append("timelineCategory", currentTimeline.tab); formData.append("regionName", "General"); addPosts(formData) .then(() => { setPosts([]); setCurrentPage(0); }) .then(handleClose); }; const onDeletePost = uuid => { deletePost(uuid).then(() => { setPosts(prevPosts => prevPosts.filter(item => item.uuid !== uuid)); }); }; return ( <> <Box pl={2} pr={2}> <Grid container justify="center" className={style.fullWidth}> <NoSsr> <InfiniteScroll style={{ width: "100%" }} dataLength={posts.length} next={fetchMoreData} hasMore={hasMore} loader={ <Box p={2} textAlign="center"> <Typography variant="subtitle2"> Meer laden... </Typography> </Box> } endMessage={ <Box p={2} textAlign="center"> <Typography variant="subtitle2"> Alle berichten geladen. </Typography> </Box> } > <Suspense fallback={ <span>Gebruikergegevens inladen...</span> } > {posts.map((item: Post) => ( <Grid item xs={12} key={item.uuid}> <PostComponent post={item} onDeletePost={onDeletePost} /> </Grid> ))} </Suspense> </InfiniteScroll> </NoSsr> </Grid> </Box> <TimelineFooter handleClickOpen={handleClickOpen} /> <PostCreation open={open} handleClose={handleClose} handlePostCreation={handlePostCreation} /> </> ); } export default Timeline;
核心修改点
- 移除了冗余的监听
posts+currentPage的useEffect,彻底解决联动触发重复请求的问题 fetchPosts使用useCallback包裹,内部修改posts用函数式更新,解决闭包陷阱,无需再禁用react-hooks/exhaustive-deps规则- 仅用两个useEffect就覆盖所有需求:
currentTimeline变更时重置状态,currentPage变更时触发请求,自动覆盖首次加载、翻页、切换时间线三个场景 - 发布帖子后的重置逻辑也同步调整为直接修改
currentPage触发请求,逻辑统一
内容的提问来源于stack exchange,提问作者MarioVde
相关产品推荐
相关产品推荐

