2024-04-11 Remix 改变了我编写 dialog 的方式


关于 dialog(对话框),回想一下,我的代码经历过好几次变化,使用 React 之前,React 初级阶段,目前部分项目使用原生 HTML 部分项目使用 Remix framework。

tl;dr

  1. React 之前:命令式,例如 window.alert(message)
  2. React 初期:all in 声明式,例如:[isOpen, setIsOpen] = useState(true)
  3. 采用 Radix UI 阶段:借助 DialogTrigger
  4. daisyUI 相关项目: 回归 HTML native dialog
  5. Remix 相关项目:设计 Modal Route

React 之前:命令式

React 之前使用过 jQuery, Ext JS, pure JS 编写 UI,一般使用命令式编写 dialog UI。

// jQuery code
$("#boring").click(function() {
  $.dialog({
    "body": "jQueryScript.net!",
    "title": "jQuery Dialog Plugin Demo",
    "show": true
  });
});

// pure JS
window.alert("hello world")

// sweetalert2
Swal.fire({
  title: "Good job!",
  text: "You clicked the button!",
  icon: "success"
});

React 初期:all in 声明式

刚开始使用 React 时,几乎都有代码都是声明式的,一般如下:

  let [isOpen, setIsOpen] = useState(true)

  function closeModal() {
    setIsOpen(false)
  }

  function openModal() {
    setIsOpen(true)
  }


  return (
    <>
      <button onClick={openModal}>open</button>
      <Dialog open={isOpen} onClose={closeModal} >
        {* ... *}
      </Dialog>
    </>
  ) 

不过这种写法有很多问题:

  1. 如果只是想实现类似 alert 的提醒功能,useState 一套下来,无用代码爆炸
  2. 有时需要在 API request 过程中处理 dialog,但是 dialog 显示与否被绑定在 UI 中, 不利于代码拆分。
    1. 根据 API request 结果处理 dialog 也需要写一堆 if-else

采用 Radix UI 阶段:借助 DialogTrigger

<AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="outline">Show Dialog</Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
      {* ... *}
      </AlertDialogContent>
</AlertDialog>

使用一个 AlertDialogTrigger 可以避免 useState 爆炸,算是一点点改善🤏

daisyUI 相关项目: 回归 HTML native dialog


<dialog id="my_modal_2" className="modal">
  <div className="modal-box">
    <h3 className="font-bold text-lg">Hello!</h3>
    <p className="py-4">Press ESC key or click outside to close</p>
  </div>
  <form method="dialog" className="modal-backdrop">
    <button>close</button>
  </form>
</dialog>

// close dialog
document.getElementById('my_modal_2').showModal()

这种方式不仅避免了 useState 爆炸,又可以在任意时机操作 dialog。很棒。

缺点就是在 React 项目中需要一个 useRef 配合。

  const dialogRef = useRef<HTMLDialogElement>(null);

  function open() {
    dialogRef.current?.showModal();
  }

  <dialog ref={dialogRef} className="modal">
    {* ... *}
  </dialog>

Remix 相关项目:设计 Modal Route

在需要使用 Modal 的页面中埋下一个 <Outlet />,然后实现一个 Modal Route 如下:

// members.$id.tsx
<Link to={"disable"}>disable</Link>

// members.$id.disable.tsx
export async function action() {
  // your code
  
  return redirect("/members/${id}")
}

export default function MemberDisableRoute() {
    const navigate = useNavigate()
    return (
        <dialog id={props.title} className="modal modal-open">
            <div className="modal-box">
                <h3 className="font-bold text-lg">Are you sure?</h3>
                <div className="modal-action">
                    <button className="btn" onClick={() => navigate(-1)}>Cancel</button>
                    <Form method="POST">
                        <input name="id" value="id" hidden />
                        <button className="btn">Yes</button>
                    </Form>
                </div>
            </div>
        </dialog>
    );
}

不仅没有 useState 数量爆炸,也不需要手工维护 dialog 状态。

其他优点:

  1. 更合理的拆分业务逻辑,disable UI 和 API 和 member detail 完全隔离,更容易测试。
  2. 更容易处理 page 状态。例如多个表单的提交状态,以前常常会出现 form1.submitting || form2.submitting 的问题,现在只需要进行页面跳转即可。
  3. 更容易处理多级弹窗。只需要简单重复 Outlet 即可,避免了多层嵌套。

缺点:

  • 框架依赖。这是 Remix Nested Routes 前提下的实现方案,不是 Web Standards。

背景资料: