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;
优化方案
方案一:父组件管理状态,子组件负责数据请求(修复现有逻辑)
先解决原代码中的明显问题:
sendData={setData()}错误地直接调用了函数,应该传递函数引用而非执行结果- 子组件
useEffect中data未定义,应改为eventData - 缺少分页偏移量(offset)参数,导致每次加载更多都会重复请求相同数据
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
相关产品推荐
相关产品推荐

