如何在不刷新父组件的情况下加载React Modal并填充异步数据
问题描述
我一直想实现用异步数据填充Modal,同时不导致父页面重渲染。我尝试为Modal创建单独的Context,将Modal移出父组件,但仍需要从父组件内点击打开Modal。
卡片组件(Card Component)
import React, { useContext, useState, useRef, memo } from "react"; import { useNavigate } from "react-router-dom"; import { ShowProvider } from "../../ModalContext"; import { ShowContext } from "../../ModalContext"; import uniqid from "uniqid"; import { getText } from "../../helpers/getData"; import Modal from "../Modal/Modal"; import "./Card.css"; const customStyles = { content: { width: "70vw", height: "60vh", top: "50%", left: "50%", right: "auto", bottom: "auto", marginRight: "-50%", transform: "translate(-50%, -50%)", }, }; const Card = (props) => { const navigate = useNavigate(); const { showData, setShowData } = useContext(ShowContext); const img = props.book.book_image; const title = props.book.title; // 获取ID后调用接口获取单个描述 const getSummary = async (title) => { const url = `https://www.googleapis.com/books/v1/volumes?q=${title}&key=${process.env.REACT_APP_GOOGLE_KEY}`; const response = await getText(url); const description = response.data.items[0].volumeInfo.description; console.log(description); setShowData({ description: description }); console.log(showData); }; const open = () => { console.log("open"); setShowData({ show: true }); getSummary(title); }; const closed = () => { console.log("close"); setShowData({ ...showData, show: false }); }; return ( <> <main> <Modal show={showData.show} handleClose={closed}> <p> {showData.description}</p> </Modal> <button type="button" className="open" onClick={open}> Description </button> <li className="card"> <img src={img} alt="book cover" key={uniqid()}></img> </li> </main> </> ); }; export default Card;
父组件(Parent Component)
import React, { useEffect, useContext } from "react"; import { Context } from "../../../Context"; import "./Best.css"; import { getData } from "../../../helpers/getData"; import Card from "../.././Card/Card"; import Results from "../.././Results/Results"; import AnimatedLayout from "../../AnimatedLayout"; const Best = () => { const { data, setData } = useContext(Context); const getCurrent = async () => { const url = `https://api.nytimes.com/svc/books/v3/lists/hardcover-fiction.json?api-key=XYZ&pages=1`; const response = await getData(url); if (response.message) { console.log(response.message); setData({ ...data, error: response.message }); alert(response.message); } console.log(response); const newArr = data.results.slice(); const newData = [...newArr, ...response]; setData({ ...data, results: [...newData] }); }; useEffect(() => { setData({ ...data, results: [] }); getCurrent(); }, []); return ( <Results> <AnimatedLayout> <ul className="results-ul horizontal"> <li className="error fade">{data.error}</li> {data.results.map((book) => { return <Card book={book}>CARD</Card>; })} </ul> </AnimatedLayout> </Results> ); }; export default Best;
解决方案
1. 让Card独立管理Modal状态
当前全局ShowContext会导致所有Card响应状态变化,改成Card内部维护Modal状态,配合memo避免不必要重渲染:
import React, { useState, memo } from "react"; import { useNavigate } from "react-router-dom"; import uniqid from "uniqid"; import { getText } from "../../helpers/getData"; import Modal from "../Modal/Modal"; import "./Card.css"; const customStyles = { content: { width: "70vw", height: "60vh", top: "50%", left: "50%", right: "auto", bottom: "auto", marginRight: "-50%", transform: "translate(-50%, -50%)", }, }; const Card = memo((props) => { const navigate = useNavigate(); // 组件内部维护Modal状态 const [modalState, setModalState] = useState({ show: false, description: "" }); const img = props.book.book_image; const title = props.book.title; const getSummary = async (title) => { try { const url = `https://www.googleapis.com/books/v1/volumes?q=${title}&key=${process.env.REACT_APP_GOOGLE_KEY}`; const response = await getText(url); const description = response.data.items[0]?.volumeInfo.description || "暂无描述"; // 基于旧状态更新,避免覆盖其他属性 setModalState(prev => ({ ...prev, description })); } catch (err) { console.error("获取描述失败:", err); setModalState(prev => ({ ...prev, description: "获取描述失败" })); } }; const open = () => { setModalState(prev => ({ ...prev, show: true })); getSummary(title); }; const closed = () => { setModalState(prev => ({ ...prev, show: false })); }; return ( <> <Modal show={modalState.show} handleClose={closed}> <p>{modalState.description}</p> </Modal> <button type="button" className="open" onClick={open}> 查看描述 </button> <li className="card"> <img src={img} alt="book cover" key={uniqid()}></img> </li> </> ); }); export default Card;
2. 优化父组件状态更新逻辑
父组件中优化setData写法,避免不必要的状态覆盖,同时用useCallback稳定函数引用:
import React, { useEffect, useContext, useCallback } from "react"; import { Context } from "../../../Context"; import "./Best.css"; import { getData } from "../../../helpers/getData"; import Card from "../../Card/Card"; import Results from "../../Results/Results"; import AnimatedLayout from "../../AnimatedLayout"; const Best = () => { const { data, setData } = useContext(Context); // useCallback稳定函数引用,避免useEffect重复触发 const getCurrent = useCallback(async () => { try { const url = `https://api.nytimes.com/svc/books/v3/lists/hardcover-fiction.json?api-key=XYZ&pages=1`; const response = await getData(url); if (response.message) { setData(prev => ({ ...prev, error: response.message })); alert(response.message); return; } // 基于旧状态拼接新数据,避免覆盖其他属性 setData(prev => ({ ...prev, results: [...prev.results, ...response] })); } catch (err) { console.error("获取数据失败:", err); setData(prev => ({ ...prev, error: "获取数据失败" })); alert("获取数据失败"); } }, [setData]); useEffect(() => { // 初始化状态时直接重置,无需解构旧data setData({ results: [], error: "" }); getCurrent(); }, [getCurrent, setData]); return ( <Results> <AnimatedLayout> <ul className="results-ul horizontal"> {data.error && <li className="error fade">{data.error}</li>} {/* 给Card添加唯一key,避免列表重渲染问题 */} {data.results.map((book) => ( <Card key={book.id} book={book}>CARD</Card> ))} </ul> </AnimatedLayout> </Results> ); }; export default Best;
3. 全局Modal方案(完全移出父组件)
如果要把Modal放在全局层级,通过Context传递控制逻辑,彻底隔离Modal与父组件的渲染关联:
// ModalContext.js import React, { createContext, useContext, useState } from "react"; import Modal from "../Modal/Modal"; const ModalContext = createContext(); export const ModalProvider = ({ children }) => { const [modalState, setModalState] = useState({ show: false, content: "" }); // 接收异步获取内容的函数,打开Modal时自动加载 const openModal = async (fetchContent) => { setModalState(prev => ({ ...prev, show: true })); try { const content = await fetchContent(); setModalState(prev => ({ ...prev, content })); } catch (err) { setModalState(prev => ({ ...prev, content: "加载失败" })); } }; const closeModal = () => { setModalState({ show: false, content: "" }); }; return ( <ModalContext.Provider value={{ openModal, closeModal }}> {children} {/* 全局唯一的Modal组件 */} <Modal show={modalState.show} handleClose={closeModal}> <p>{modalState.content}</p> </Modal> </ModalContext.Provider> ); }; export const useModal = () => useContext(ModalContext);
修改后的Card组件:
import React, { memo } from "react"; import { useNavigate } from "react-router-dom"; import uniqid from "uniqid"; import { getText } from "../../helpers/getData"; import { useModal } from "../../ModalContext"; import "./Card.css"; const Card = memo((props) => { const navigate = useNavigate(); const { openModal } = useModal(); const img = props.book.book_image; const title = props.book.title; // 定义异步获取描述的逻辑 const fetchDescription = async () => { const url = `https://www.googleapis.com/books/v1/volumes?q=${title}&key=${process.env.REACT_APP_GOOGLE_KEY}`; const response = await getText(url); return response.data.items[0]?.volumeInfo.description || "暂无描述"; }; const open = () => { // 调用全局Modal的打开方法,传入获取逻辑 openModal(fetchDescription); }; return ( <> <button type="button" className="open" onClick={open}> 查看描述 </button> <li className="card"> <img src={img} alt="book cover" key={uniqid()}></img> </li> </> ); }); export default Card;
以上方案均可实现:异步填充Modal时,父组件和其他未操作的Card不会重渲染,同时保持从父组件内点击打开Modal的交互逻辑。
内容的提问来源于stack exchange,提问作者Daniel Brusky
相关产品推荐
相关产品推荐

