【React】封装一个好用方便的消息框(Hooks Bootstrap 实践)

news2025/1/20 10:47:39

引言

以 Bootstrap 为例,使用模态框编写一个简单的消息框:

import { useState } from "react";
import { Modal } from "react-bootstrap";
import Button from "react-bootstrap/Button";
import 'bootstrap/dist/css/bootstrap.min.css';

function App() {
  let [show, setShow] = useState(false);
  const handleConfirm = () => {
    setShow(false);
    console.log("confirm");
  };
  const handleCancel = () => {
    setShow(false);
    console.log("cancel");
  };


  return (
    <div>
      <Button variant="primary" onClick={() => setShow(true)}>弹窗</Button>
      <Modal show={show}>
        <Modal.Header>
          <Modal.Title>我是标题</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          Hello World
        </Modal.Body>
        <Modal.Footer>
          <Button variant="primary" onClick={handleConfirm}>确定</Button>
          <Button variant="secondary" onClick={handleCancel}>取消</Button>
        </Modal.Footer>
      </Modal>
    </div>
  );
}

export default App;

整段代码十分复杂。

Bootstrap 的模态框使用 show 属性决定是否显示,因此我们不得不创建一个 state 来保存是否展示模态框。然后还得自己手动在按钮的点击事件里控制模态框的展示。

如果你编写过传统桌面软件,弹一个消息框应该是很简单的事情,就像

if (MessageBox.show('我是标题', 'HelloWorld', MessageBox.YesNo) == MessageBox.Yes)
	console.log('确定');
else
	console.log('取消');

一样。

那么下面我们就朝着这个方向,尝试将上面的 React 代码简化。

0. 简单封装

首先从 HTML 代码开始简化。先封装成一个简单的受控组件:

import React, { useMemo } from "react";
import { useState, createContext, useRef } from "react";
import { Button, Modal } from "react-bootstrap";

/**
 * 类 Windows 消息框组件。
 * @param {object} props 
 * @param {string} props.title 消息框标题
 * @param {string} props.message 消息框内容
 * @param {string} [props.type="ok"] 消息框类型
 * @param {boolean} [props.showModal=false] 是否显示消息框
 * @param {function} [props.onResult] 消息框结果回调
 * @returns {JSX.Element}
 */
function MessageBox(props) {
    let title = props.title;
    let message = props.message;
    let type = props.type || 'ok';
    let showModal = props.showModal || false;
    let onResult = props.onResult || (() => {});

    let buttons = null;

    // 处理不同按钮
    const handleResult = (result) => {
        onResult(result);
    };
    if (type === 'ok') {
        buttons = (
            <Button variant="primary" onClick={ () => handleResult('ok') }>确定</Button>
        );
    }
    else if (type === 'yesno') {
        buttons = (
            <>
                <Button variant="secondary" onClick={ () => handleResult('confirm') }>取消</Button>
                <Button variant="primary" onClick={ () => handleResult('cancel') }>确定</Button>
            </>
        )
    }

    return (
        <div>
            <Modal show={showModal}>
                <Modal.Header>
                    <Modal.Title>{title}</Modal.Title>
                </Modal.Header>
                <Modal.Body>{message}</Modal.Body>
                <Modal.Footer>
                    {buttons}
                </Modal.Footer>
            </Modal>
        </div>
    );
}

export default MessageBox;

测试:

function App() {
  const handleResult = (result) => {
    console.log(result);
  };

  return (
    <div>
      <MessageBox showModal={true} title="我是标题" message="Hello World" type="ok" onResult={handleResult} />
    </div>
  );
}

在这里插入图片描述
HTML 代码部分简化完成。这下代码短了不少。
现在如果想要正常使用消息框,还需要自己定义 showModal 状态并绑定 onResult 事件控制消息框的显示隐藏。下面我们来简化 JS 调用部分。

1. useContext

首先可以考虑全局都只放一份模态框的代码到某个位置,然后要用的时候都修改这一个模态框即可。这样就不用每次都写一个 <MessageBox ... /> 了。

为了能在任意地方都访问到模态框,可以考虑用 Context 进行跨级通信。
把“修改模态框内容 + 处理隐藏”这部分封装成一个函数 show(),然后通过 Context 暴露出去。

import { useState, createContext, useRef, useContext } from "react";
import MessageBoxBase from "./MessageBox";

const MessageBoxContext = createContext(null);

function MessageBoxProvider(props) {
    let [showModal, setShowModal] = useState(false);

    let [title, setTitle] = useState('');
    let [message, setMessage] = useState('');
    let [type, setType] = useState(null);
    let resolveRef = useRef(null); // 因为与 UI 无关,用 ref 不用 state

    const handleResult = (result) => {
        resolveRef.current(result);
        setShowModal(false);
    };

    const show = (title, message, type) => {
        setTitle(title);
        setMessage(message);
        setType(type);
        setShowModal(true);

        return new Promise((resolve, reject) => {
            resolveRef.current = resolve;
        });
    };

    return (
        <MessageBoxContext.Provider value={show}>
            <MessageBoxBase
                title={title}
                message={message}
                type={type}
                showModal={showModal}
                onResult={handleResult}
            />
            {props.children}
        </MessageBoxContext.Provider>
    );
}

export { MessageBoxProvider, MessageBoxContext };

使用:
index.js

root.render(
  <React.StrictMode>
    <MessageBoxProvider>
      <App />
    </MessageBoxProvider>
  </React.StrictMode>
);

App.js

function App() {
  let msgBox = useContext(MessageBoxContext);
  const handleClick = async () => {
    let result = await msgBox('我是标题', 'Hello World', 'yesno');
    console.log(result);
    if (result === 'yes') {
      alert('yes');
    } else if (result === 'no') {
      alert('no');
    }
  };

  return (
    <div>
      <Button variant="primary" onClick={handleClick}>弹窗1</Button>
    </div>
  );
}

为了方便使用,可以在 useContext 之上再套一层:

/** 
 * 以 Context 方式使用 MessageBox。
 * @return {(title: string, message: string, type: string) => Promise<string>}
 */
function useMessageBox() {
    return useContext(MessageBoxContext);
}

这样封装使用起来是最简单的,只需要 useMessageBox 然后直接调函数即可显示消息框。
但是缺点显而易见,只能同时弹一个消息框,因为所有的消息框都要共享一个模态框。

2. Hook

为了解决上面只能同时弹一个框的问题,我们可以考虑取消全局只有一个对话框的策略,改成每个要用的组件都单独一个对话框,这样就不会出现冲突的问题了。

即将模态框组件和状态以及处理函数都封装到一个 Hook 里,每次调用这个 Hook 都返回一个组件变量和 show 函数,调用方只需要把返回的组件变量渲染出来,然后调用 show 即可。

import React, { useMemo } from "react";
import { useState, createContext, useRef } from "react";
import MessageBoxBase from "./MessageBox";

/**
 * 以 Hook 方式使用消息框。
 * @returns {[MessageBox, show]} [MessageBox, show]
 * @example
 * const [MessageBox, show] = useMessageBox(); 
 * return (
 *  <MessageBox />
 *  <button onClick={() => show('title', 'message', 'ok')} >show</button>
 * );
 */
function useMessageBox() {
    let [title, setTitle] = useState('');
    let [message, setMessage] = useState('');
    let [type, setType] = useState(null);

    let [showDialog, setShowDialog] = useState(false);
    let resolveRef = useRef(null);

    const handleResult = (result) => {
        resolveRef.current(result);
        setShowDialog(false);
    };

    const MessageBox = useMemo(() => { // 也可以不用 useMemo 直接赋值 JSX 代码
        return (
            <MessageBoxBase
                title={title}
                message={message}
                type={type}
                showModal={showDialog}
                onResult={handleResult}
            />
        );
    }, [title, message, type, showDialog]);

    const show = (title, message, type) => {
        setTitle(title);
        setMessage(message);
        setType(type);
        setShowDialog(true);

        return new Promise((resolve, reject) => {
            resolveRef.current = resolve;
        });
    };

    return [MessageBox, show];
}

export default useMessageBox;

App.js

function App() {
    const [MessageBox, show] = useMessageBox();
    return (
        <div>
            {MessageBox}
            <button onClick={ () => show('title', 'message', 'ok') }>HookShow1</button>
            <button onClick={ () => show('title', 'message', 'yesno') }>HookShow2</button>
        </div>
    );
}

3. forwardRef + useImperativeHandle

上面我们都是封装成 show() 函数的形式。对于简单的消息框,这种调用方式非常好用。但是如果想要显示复杂的内容(例如 HTML 标签)就有些麻烦了。

这种情况可以考虑不封装 HTML 代码,HTML 代码让调用者手动编写,我们只封装控制部分的 JS 代码,即 showModal 状态和回调函数。

如果是类组件,可以直接添加一个普通的成员方法 show(),然后通过 ref 调用这个方法。但是现在我们用的是函数式组件,函数式组件想要使用 ref 需要使用 forwardRefuseImperativeHandle 函数,具体见这里。

import { useImperativeHandle, useRef, useState } from "react";
import MessageBox from "./MessageBox";
import { forwardRef } from "react";

function MessageBoxRef(props, ref) {
    let [showModal, setShowModal] = useState(false);
    let resolveRef = useRef(null);

    function handleResult(result) {
        setShowModal(false);
        resolveRef.current(result);
    }
	
	// ref 引用的对象将会是第二个参数(回调函数)的返回值
    useImperativeHandle(ref, () => ({
        show() {
            setShowModal(true);
            return new Promise((resolve, reject) => {
                resolveRef.current = resolve;
            });
        }
    }), []); // 第三个参数为依赖,类似于 useEffect()

    return <MessageBox {...props} showModal={showModal} onResult={handleResult} />;
}

export default forwardRef(MessageBoxRef);

使用的时候只需要创建一个 ref,然后 ref.current.show() 即可。
App.js

function App() {
    const messageBoxRef = useRef();
    return (
        <div>
            <MessageBoxRef ref={messageBoxRef} title="标题" message="内容" />
            <button onClick={ () => messageBoxRef.current.show() }>RefShow</button>
        </div>
    );
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1719540.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

根据状态转移图实现时序电路 (三段式状态机)

看图编程 * ** 代码 module seq_circuit(input C ,input clk ,input rst_n,output wire Y ); reg [1:0] current_stage ; reg [1:0] next_stage ; reg Y_reg; //输出//第一段 &#xff1a; 初始化当前状态和…

TensorFlow Playground神经网络演示工具使用方法详解

在现代机器学习领域,神经网络无疑是一个重要的研究方向。然而,对于许多初学者来说,神经网络的概念和实际操作可能显得相当复杂。幸运的是,TensorFlow Playground 提供了一个交互式的在线工具,使得我们可以直观地理解和实验神经网络的基本原理。在这篇博客中,我们将详细介…

Spring Boot 项目中使用 JSP

文章目录 Spring Boot 项目中使用 JSP项目结构引入依赖包编写页面和后台运行方式一&#xff1a;Maven 命令运行方式二&#xff1a;在 IDEA 中运行方式三&#xff1a;打 war 包部署运行 Spring Boot 项目中使用 JSP 在 Spring Boot 项目中不是不可以使用 JSP 。想在 Spring Boo…

Fully Convolutional Networks for Semantic Segmentation--论文笔记

论文笔记 资料 1.代码地址 2.论文地址 https://arxiv.org/abs/1411.4038 3.数据集地址 论文摘要的翻译 卷积网络是强大的视觉模型&#xff0c;可以产生特征层次结构。我们表明&#xff0c;卷积网络本身&#xff0c;经过端到端&#xff0c;像素对像素的训练&#xff0c;在…

【NPS】微软NPS配置802.1x,验证域账号,动态分配VLAN(有线网络篇)

上两篇中介绍了如何配置NPS和在WLC上如何配置802.1X来实现验证域账号和动态分配VLAN&#xff0c;802.1x协议作为一种成熟的身份验证框架&#xff0c;不仅适用于无线网络&#xff0c;同样也适用于有线网络环境。这里我们将介绍如何在有线网络中部署802.1x认证&#xff0c;以验证…

IDEA调试前端html报错

IDEA调试前端html报错 报错如下&#xff1a; Waiting for connection to localhost:59004. Please ensure that the browser was started successfully with remote debugging port opened. Port cannot be opened if Chrome having the same User Data Directory is already …

代码随想录算法训练营第三十二 | ● 122.买卖股票的最佳时机II ● 55. 跳跃游戏 ● 45.跳跃游戏II

122.买卖股票的最佳时机II 讲解链接&#xff1a;https://programmercarl.com/1005.K%E6%AC%A1%E5%8F%96%E5%8F%8D%E5%90%8E%E6%9C%80%E5%A4%A7%E5%8C%96%E7%9A%84%E6%95%B0%E7%BB%84%E5%92%8C.html 简单思路&#xff1a;逐个计算连续两天的股票差值&#xff0c;sum初始为零&…

今日学会的,刘姥姥进大观园

Git - First-Time Git Setup 下载了Git&#xff0c;会用Git了&#xff1f; 还有这个&#xff1a;学习 HTML5 Canvas 这一篇文章就够了 | 菜鸟教程 (runoob.com) JavaScript 用法 | 菜鸟教程 (runoob.com) 看到这个真的是受益匪浅&#xff0c;我终于懂了一直有的疑惑。 3D可…

生产问题(十六)数据丢失-mysql binlog排查

一、引言 最近作者遇到一个线上问题&#xff0c;是mysql 的数据丢失了&#xff0c;这里记录一下排查过程和binlog的分析。 二、问题 问题出现的表象是应用系统大量报错&#xff0c;各种空指针之类的&#xff0c;这种一看就不可能是代码发布的问题&#xff0c;原因一定在框架、…

springboot+vue 社区养老服务系统

Springbootvue社区居家养老服务系统&#xff0c;数据库mysql&#xff0c;mybatis框架&#xff0c;有可视化页面。 功能&#xff1a; 用户管理 养老服务管理 护理人员管理 服务类型管理 健康状况管理 社区管理 服务区管理 娱乐资讯管理 咨询分类管理 反馈建议 系统简历管理 轮播…

华为telnet的两种认证方式

华为telnet的两种认证方式 实验拓扑&#xff1a; 实验要求&#xff1a; 1.采用普通密码认证实现telnet 远程登录机房设备R3 2.采用AAA认证服务方式实现telnet 远程登录机房设备R3 实验步骤&#xff1a; 1.完成基本配置&#xff08;设备接口配置IP&#xff0c;此步骤略过&#…

2024上海中小学生古诗文大会方案已发布,家长孩子最关心10个问题

昨天&#xff08;2024年5月30日&#xff09;下午15点&#xff0c;上海中小学生古诗文大会组委会通过两个公众号发布了《2024上海中小学生古诗文大会系列活动方案出炉》的推文&#xff08;下称《方案》&#xff09;。如我之前的分析和预测&#xff0c;5月份会发布今年的中小学生…

【如何用爬虫玩转石墨文档?】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

机器人动力学模型与MATLAB仿真

机器人刚体动力学由以下方程控制&#xff01;&#xff01;&#xff01; startup_rvc mdl_puma560 p560.dyn 提前计算出来这些“disturbance”&#xff0c;然后在控制环路中将它“抵消”&#xff08;有时候也叫前馈控制&#xff09; 求出所需要的力矩&#xff0c;其中M项代表克服…

判断自守数-第13届蓝桥杯选拔赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第75讲。 判断自守数&#…

【Qt秘籍】[006]-Qt 的 Hello World程序-编程第一步

"Hello,World!" 中文意思是“你好&#xff0c;世界”。 因为 The C Programming Language 中使用它做为第一个演示程序&#xff0c;后来很多程序员在学习编程或进行设备调试时延续了这一习惯。 下面&#xff0c;我们也将演示Qt中的"Hello World!" 我们先创…

分享一个 ASP.NET Web Api 上传和读取 Excel的方案

前言 许多业务场景下需要处理和分析大量的数据&#xff0c;而 Excel 是业务人员常用的数据表格工具&#xff0c;因此&#xff0c;将 Excel 表格中内容上传并读取到网站&#xff0c;是一个很常见的功能&#xff0c;目前有许多成熟的开源或者商业的第三方库&#xff0c;比如 NPO…

计算机视觉与模式识别实验1-2 图像的形态学操作

文章目录 &#x1f9e1;&#x1f9e1;实验流程&#x1f9e1;&#x1f9e1;1.图像膨胀2.图像腐蚀3.膨胀与腐蚀的综合使用4.对下面二值图像的目标提取骨架&#xff0c;并分析骨架结构。 &#x1f9e1;&#x1f9e1;全部代码&#x1f9e1;&#x1f9e1; &#x1f9e1;&#x1f9e1…

【Nginx】Nginx 日志配置

Nginx 日志配置 Nginx 主要有两种日志类型&#xff1a;访问日志&#xff08;access log&#xff09;和错误日志&#xff08;error log&#xff09;&#xff0c;可以帮助监控和调试服务的运行。 1.访问日志 访问日志主要记录客户端的请求&#xff0c;客户端向 Nginx 发起的每…

jsp实验18 JDBC

源代码以及执行结果截图&#xff1a; admoinStudent.jsp <% page language"java" contentType"text/html; charsetutf-8" pageEncoding"utf-8"%> <%page import"java.sql.*"%> <% page import"javax.sql.DataS…