You need to enable JavaScript to run this app. 如何配置React useEffect实现多依赖数据请求并避免重复接口调用?
优惠活动
大模型
产品
解决方案
定价
更多

如何配置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.19 12:15:05