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

如何在不刷新父组件的情况下加载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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 12:47:04