当前位置: 首页 > news >正文

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

引言

以 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('确定');
elseconsole.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 不用 stateconst 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}><MessageBoxBasetitle={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 (<MessageBoxBasetitle={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>);
}

相关文章:

  • oracle创建新用户,并且只给新用户赋予查询权限
  • 2024Dragon Knight CTF复现web
  • 11.1 排序算法
  • C++中的智能指针类别
  • 汽车MCU虚拟化--对中断虚拟化的思考(1)
  • 利用GNSS IMU集成提高车道级定位精度
  • Blueprints - Collision Presets相关
  • 4月啤酒品类线上销售数据分析
  • Java-集合基础
  • LeetCode # 1070. 产品销售分析 III
  • Python列表
  • RustDesk服务器
  • 整理GTX收发器示例工程(高速收发器十一)
  • 医院该如何应对网络安全?
  • Redis缓存(笔记一:缓存介绍和数据库启动)
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • Dubbo 整合 Pinpoint 做分布式服务请求跟踪
  • IndexedDB
  • java8 Stream Pipelines 浅析
  • js递归,无限分级树形折叠菜单
  • js正则,这点儿就够用了
  • Mysql优化
  • TCP拥塞控制
  • Twitter赢在开放,三年创造奇迹
  • vue2.0项目引入element-ui
  • 看图轻松理解数据结构与算法系列(基于数组的栈)
  • 深度学习入门:10门免费线上课程推荐
  • 项目实战-Api的解决方案
  • 一文看透浏览器架构
  • 怎样选择前端框架
  • 智能合约Solidity教程-事件和日志(一)
  • 智能合约开发环境搭建及Hello World合约
  • ​经​纬​恒​润​二​面​​三​七​互​娱​一​面​​元​象​二​面​
  • # include “ “ 和 # include < >两者的区别
  • (11)工业界推荐系统-小红书推荐场景及内部实践【粗排三塔模型】
  • (2.2w字)前端单元测试之Jest详解篇
  • (7)STL算法之交换赋值
  • (LeetCode 49)Anagrams
  • (Mac上)使用Python进行matplotlib 画图时,中文显示不出来
  • (poj1.2.1)1970(筛选法模拟)
  • (zt)最盛行的警世狂言(爆笑)
  • (二)pulsar安装在独立的docker中,python测试
  • (二)springcloud实战之config配置中心
  • (十)DDRC架构组成、效率Efficiency及功能实现
  • (四)linux文件内容查看
  • (学习日记)2024.03.12:UCOSIII第十四节:时基列表
  • (幽默漫画)有个程序员老公,是怎样的体验?
  • (原創) X61用戶,小心你的上蓋!! (NB) (ThinkPad) (X61)
  • (中等) HDU 4370 0 or 1,建模+Dijkstra。
  • (转)重识new
  • .net 4.0 A potentially dangerous Request.Form value was detected from the client 的解决方案
  • .net core webapi 大文件上传到wwwroot文件夹
  • .NET Core中如何集成RabbitMQ
  • .NET MVC之AOP
  • .NET/C# 阻止屏幕关闭,阻止系统进入睡眠状态