You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React可复用组件父子数据流实现困惑及方案咨询

React可复用组件实现方案优化

问题背景

开发React可复用组件时碰到以下需求与困惑:

  • 组件需从API获取数据并传递给父组件,由父组件映射为卡片展示
  • 组件要接收父组件的「加载更多」函数调用,同时支持控制查询条件的props
  • 纠结最优实现方式,也考虑过通过props.children在子组件内直接展示卡片,避免向上传递数据

现有代码

父组件(Page)

function Page() {
    const [eventData, setEventData] = useState([]);

    const EventCardUpcoming = () => {
        const filterRef = useRef(null);

        const handleClick = () => {
            filterRef.current.callLoad();
        };

        const setData = () => {
            setEventData()
        }

        return (
            <InfiniteScroll
                pageStart={0}
                hasMore={true || false}
                loadMore={handleClick}
            >
                <button onClick={handleClick}>Load more</button>
                <Row className="py-4">
                    <div>
                        <EventFiltering
                            OrganizerId={"1"}
                            ref={filterRef}
                            sendData={setData()}
                        />
                    </div>
                    {eventData ? 
                    <div>
                        {eventData.map((v, i) => (
                            <Col key={i} xs={12} sm={12} md={6} lg={4} xl={3}>
                                <div className="d-flex justify-content-center">
                                    <>
                                        <EventCard eventData={v} />
                                    </>
                                </div>
                            </Col>
                        ))}
                    </div>
                    : null}
                </Row>
            </InfiniteScroll>
        );
    };
    return (
        <Container>
            <EventCardUpcoming />
        </Container>
    );
}

export default Page;

子组件(Filtering)

const Filtering = forwardRef((props, ref, { sendData }) => {
    const staticOrganizer = props.OrganizerId; //API configurable option
    
    const paramLimit = 4;
    const [eventData, setEventData] = useState([]);

    let params = {
        org: staticOrganizer,
        limit: paramLimit,
    };

    useImperativeHandle(ref, () => ({
        callLoad() {
            handleLoadMore(); //load more data from function called in parent
        },
    }));

    useEffect(() => {
        sendData(data); //send data to parent when more is fecthed
    }, [eventData]);

    const handleLoadMore = () => {
        APIGet("getData", params).then((res) => {
            if (res.data.body.length > 0) {
                setEventData((eventData) => eventData.concat(res.data.body));
            }
        });
    };

        return (
            <Container>
                <div>
                    <Dropdowns />
                </div>
            </Container>
        );
    }
);

export default Filtering;

优化方案

方案一:父组件管理状态,子组件负责数据请求(修复现有逻辑)

先解决原代码中的明显问题:

  1. sendData={setData()} 错误地直接调用了函数,应该传递函数引用而非执行结果
  2. 子组件useEffect中data未定义,应改为eventData
  3. 缺少分页偏移量(offset)参数,导致每次加载更多都会重复请求相同数据
  4. setEventData()未传入更新值,无法更新父组件状态

优化后的代码:

父组件

function Page() {
    const [eventData, setEventData] = useState([]);
    const [hasMore, setHasMore] = useState(true);

    const EventCardUpcoming = () => {
        const filterRef = useRef(null);

        const handleLoadMore = () => {
            filterRef.current?.callLoad();
        };

        // 接收子组件传递的新数据,合并到现有状态
        const updateEventData = (newData) => {
            setEventData(prev => [...prev, ...newData]);
            // 如果返回数据为空,标记没有更多数据
            if (newData.length === 0) setHasMore(false);
        };

        return (
            <InfiniteScroll
                pageStart={0}
                hasMore={hasMore}
                loadMore={handleLoadMore}
            >
                <button onClick={handleLoadMore} disabled={!hasMore}>
                    {hasMore ? "Load more" : "No more data"}
                </button>
                <Row className="py-4">
                    <div>
                        <EventFiltering
                            OrganizerId={"1"}
                            ref={filterRef}
                            sendData={updateEventData}
                        />
                    </div>
                    <div>
                        {eventData.map((v, i) => (
                            <Col key={v.id || i} xs={12} sm={12} md={6} lg={4} xl={3}>
                                <div className="d-flex justify-content-center">
                                    <EventCard eventData={v} />
                                </div>
                            </Col>
                        ))}
                    </div>
                </Row>
            </InfiniteScroll>
        );
    };

    return (
        <Container>
            <EventCardUpcoming />
        </Container>
    );
}

export default Page;

子组件

const Filtering = forwardRef((props, ref) => {
    const { OrganizerId, sendData } = props;
    const paramLimit = 4;
    const [offset, setOffset] = useState(0);
    const [isLoading, setIsLoading] = useState(false);

    // 分页参数随offset更新
    const params = useMemo(() => ({
        org: OrganizerId,
        limit: paramLimit,
        offset: offset
    }), [OrganizerId, offset]);

    useImperativeHandle(ref, () => ({
        async callLoad() {
            // 避免重复请求
            if (isLoading) return;
            setIsLoading(true);
            try {
                const res = await APIGet("getData", params);
                const newData = res.data.body || [];
                sendData(newData);
                // 更新偏移量
                setOffset(prev => prev + paramLimit);
            } catch (err) {
                console.error("Load more failed:", err);
            } finally {
                setIsLoading(false);
            }
        },
    }));

    // 初始化加载第一页数据
    useEffect(() => {
        ref.current?.callLoad();
    }, []);

    return (
        <Container>
            <div>
                <Dropdowns />
            </div>
        </Container>
    );
});

export default Filtering;

方案二:子组件内渲染卡片(通过Render Prop/Children函数)

这种方式让子组件管理数据状态,父组件只提供卡片的渲染逻辑,减少状态向上传递的复杂度,更符合组件封装性。

子组件(修改后)

const Filtering = ({ OrganizerId, children }) => {
    const paramLimit = 4;
    const [eventData, setEventData] = useState([]);
    const [offset, setOffset] = useState(0);
    const [hasMore, setHasMore] = useState(true);
    const [isLoading, setIsLoading] = useState(false);

    const params = useMemo(() => ({
        org: OrganizerId,
        limit: paramLimit,
        offset: offset
    }), [OrganizerId, offset]);

    const handleLoadMore = async () => {
        if (isLoading || !hasMore) return;
        setIsLoading(true);
        try {
            const res = await APIGet("getData", params);
            const newData = res.data.body || [];
            setEventData(prev => [...prev, ...newData]);
            setOffset(prev => prev + paramLimit);
            if (newData.length === 0) setHasMore(false);
        } catch (err) {
            console.error("Load more failed:", err);
        } finally {
            setIsLoading(false);
        }
    };

    // 初始化加载
    useEffect(() => {
        handleLoadMore();
    }, []);

    return (
        <Container>
            <div>
                <Dropdowns />
            </div>
            {/* 把数据和加载更多方法传递给父组件的渲染逻辑 */}
            <div>
                {children({ data: eventData, loadMore: handleLoadMore, hasMore, isLoading })}
            </div>
        </Container>
    );
};

export default Filtering;

父组件(修改后)

function Page() {
    const EventCardUpcoming = () => {
        return (
            <EventFiltering OrganizerId={"1"}>
                {({ data, loadMore, hasMore, isLoading }) => (
                    <InfiniteScroll
                        pageStart={0}
                        hasMore={hasMore}
                        loadMore={loadMore}
                    >
                        <button onClick={loadMore} disabled={!hasMore || isLoading}>
                            {isLoading ? "Loading..." : hasMore ? "Load more" : "No more data"}
                        </button>
                        <Row className="py-4">
                            {data.map((v, i) => (
                                <Col key={v.id || i} xs={12} sm={12} md={6} lg={4} xl={3}>
                                    <div className="d-flex justify-content-center">
                                        <EventCard eventData={v} />
                                    </div>
                                </Col>
                            ))}
                        </Row>
                    </InfiniteScroll>
                )}
            </EventFiltering>
        );
    };

    return (
        <Container>
            <EventCardUpcoming />
        </Container>
    );
}

export default Page;

方案选择建议

  • 如果父组件需要对数据做额外处理(比如和其他状态合并、二次过滤),选方案一,父组件掌控数据所有权
  • 如果组件更偏向独立封装,希望减少父子组件间的状态传递,选方案二,子组件管理数据,父组件只负责UI渲染

内容的提问来源于stack exchange,提问作者Theo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 14:10:29