React类组件如何将Modal抽离为独立可复用文件并共享状态?
问题原因
- 你抽离的
ItemModal组件没有初始化内部state,直接访问this.state.isOpen自然会抛出null错误 - Modal的显隐状态不应该由子组件自身维护,应该由触发它的父组件托管,符合React受控组件的设计规范,也方便跨场景复用
- 原有父组件的逻辑存在缺陷:只维护了一个
isOpen状态,无法记录当前点击的对应item数据,且在map中重复渲染多个Modal会造成性能浪费
改造方案
第一步:修改独立抽离的ItemModal组件
组件改为完全受控模式,所有配置、状态、回调都通过父组件传入的props获取,不需要内部维护私有状态,保证通用性:
import React from 'react'; import "./Item.css"; import Modal from 'react-modal'; const customStyles = { content: { top: '50%', left: '50%', right: 'auto', bottom: 'auto', marginRight: '-50%', transform: 'translate(-50%, -50%)', }, }; Modal.setAppElement('#root'); class ItemModal extends React.Component { render() { let subtitle; function afterOpenModal() { subtitle.style.color = '#f00'; } return ( <Modal isOpen={this.props.isOpen} onAfterOpen={afterOpenModal} onRequestClose={this.props.onClose} style={customStyles} contentLabel={this.props.item?.itemNumber || '商品详情'} > <h2 ref={(_subtitle) => (subtitle = _subtitle)}>Hello</h2> <button onClick={this.props.onClose}>close</button> {/* 直接通过this.props.item访问当前选中的所有商品数据 */} <div>当前商品编号:{this.props.item?.itemNumber}</div> <div>商品价格:{this.props.item?.price}</div> <form> <input /> <button>tab navigation</button> <button>stays</button> <button>inside</button> <button>the modal</button> </form> </Modal> ); } } export default ItemModal;
第二步:修改父组件FeaturedCards逻辑
父组件新增选中商品的状态存储,统一控制Modal的显隐,不需要循环渲染多个Modal实例:
import React from 'react'; import { Card, CardImg, CardTitle, CardText, CardGroup, CardSubtitle, CardBody, CardFooter, CardHeader } from 'reactstrap'; import Config from 'config'; import { decompressToBase64, formatter } from './common.js' import "./Item.css"; // 引入抽离的Modal组件 import ItemModal from './ItemModal'; class FeaturedCards extends React.Component { constructor() { super(); this.state = { name: 'React', apiData: [], itemList: [], // 存储当前选中的商品,selectedItem非空即代表Modal打开 selectedItem: null }; // 提前绑定方法,避免render内重复绑定影响性能 this.closeModal = this.closeModal.bind(this); this.handleCardClick = this.handleCardClick.bind(this); } async componentDidMount() { const tokenString = sessionStorage.getItem("token"); const token = JSON.parse(tokenString); let headers = new Headers({ "Accept": "application/json", "Content-Type": "application/json", 'Authorization': 'Bearer ' + token.token }); const response = await fetch(Config.apiUrl + `/api/Items/GetFeaturedItems`, { method: "GET", headers: headers }); const json = await response.json(); this.setState({ itemList: json }); } // 点击卡片时记录当前选中的商品 handleCardClick(item) { this.setState({ selectedItem: item }); } // 关闭弹窗时清空选中商品 closeModal() { console.log('Clicked close button') this.setState({ selectedItem: null }); } render() { const items = this.state.itemList; const isModalOpen = !!this.state.selectedItem; return ( <div> <CardGroup className="card-group-scroll"> {items && items.map(item => <Card key={item.itemNumber} tag="a" onClick={() => this.handleCardClick(item)} style={{ cursor: "pointer" }} > <CardHeader tag="h3">Featured</CardHeader> <CardImg top className="card-picture" src={"data:image/png;base64," + decompressToBase64(item.images[0]?.compressedImageData)} id={item.itemNumber + "Img"} alt={item.itemNumber} /> <CardBody className="card-body"> <CardTitle tag="h5">{item.itemNumber}</CardTitle> <CardSubtitle tag="h6" className="mb-2 text-muted">{item.categoryName}</CardSubtitle> <CardText className="card-description">{item.itemDescription}</CardText> </CardBody> <CardFooter className="text-muted">{formatter.format(item.price)}</CardFooter> </Card> )} </CardGroup> {/* 全局只渲染一个Modal实例 */} {isModalOpen && <ItemModal isOpen={isModalOpen} item={this.state.selectedItem} onClose={this.closeModal} />} </div> ); } } export default FeaturedCards;
补充说明
- 这种受控组件的设计让Modal完全和业务逻辑解耦,你在其他场景使用的时候,只要传入对应
isOpen、item、onClose三个参数即可直接复用 - 去掉了原代码中循环渲染Modal的逻辑,全局只渲染一个Modal实例,性能更优
- 所有状态统一在父组件维护,不会出现父子组件状态不同步的问题
内容的提问来源于stack exchange,提问作者Randy
相关产品推荐
相关产品推荐

