👊 【React Day 2】Dialog 组件实现

    279

eg.

依然是一个十分常见的组件,不过使用方式和 Element 之类的不一致,我更倾向于调用某个函数来创建,而非作为固定的代码事先写在 组件 / 页面 中

支持

  • 点击遮罩关闭
  • ESC关闭
  • 多 Dialog 时的顺序关闭 (代码中的 hooks 所做的事情

Use

import Dialog from "@/components/Dialog";

function DialogContent() {
  return (
    <div className="flex-auto grid place-content-center">
      <motion.button
        whileHover={{ scale: 1.1 }}
        whileTap={{ scale: 0.9 }}
        className="text-white text-[12px] p-[6px_18px] bg-green-500 rounded-md shadow-lg shadow-green-500/70 select-none"
        onClick={() => {
          Dialog.show(<DialogContent />);
        }}
      >
        click here~
      </motion.button>
    </div>
  );
}

Dialog.show(<DialogContent />);

Source Code

"use client";

import { motion, AnimatePresence } from "framer-motion";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { Icon } from "@iconify/react";

const Dialog = ({ onDestroy, node }: { onDestroy: () => void; node?: React.ReactNode }) => {
  const [show, setShow] = useState(false);
  const index = useRef(-1);

  const close = useCallback(() => {
    hooks.splice(index.current, 1);
    removeEventListener();
    setShow(false);
    setTimeout(onDestroy, 200);
  }, [onDestroy]);

  useEffect(() => {
    setShow(true);
    index.current = hooks.push(close) - 1;
  }, [close]);
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.2 }}
          className="fixed top-0 left-0 h-full w-full bg-white bg-opacity-20 grid place-content-center"
          onClick={close}
        >
          <motion.div
            className="min-w-[500px] min-h-[400px] bg-white rounded-md flex flex-col"
            initial={{ opacity: 0, scale: 0.8 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.8 }}
            transition={{ duration: 0.2, ease: "easeInOut" }}
            onClick={e => e.stopPropagation()}
          >
            <div className="border-b border-b-solid border-b-gray-300 p-[8px_16px] flex">
              <div className="flex gap-[5px] items-center">
                <span className="text-[18px]">🧀</span>
              </div>
              <div className="ml-auto">
                <motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={close}>
                  <Icon icon="heroicons:x-circle-20-solid" className="text-[25px] text-red-500" />
                </motion.button>
              </div>
            </div>
            {node}
          </motion.div>
        </motion.div>
      )}
    </AnimatePresence>
  );
};

let hooks: (() => void)[] = [];
const closeHandle = (e: KeyboardEvent) => {
  if (e.key === "Escape") {
    hooks[hooks.length - 1]?.();
  }
  removeEventListener();
};

const removeEventListener = () => {
  hooks.length < 1 && document.removeEventListener("keydown", closeHandle);
};

const show = (component?: React.ReactNode) => {
  const dialog = document.createElement("div");
  createRoot(dialog).render(
    <Dialog
      onDestroy={() => {
        dialog.remove();
      }}
      node={component}
    />,
  );
  document.body.appendChild(dialog);
  document.addEventListener("keydown", closeHandle);
};

export default { show };

GitHub Page

https://acexiamo.github.io/learning_react

📚 End

ESlint 警告 & useEffect & useCallback

const Dialog = ({ onDestroy, node }: { onDestroy: () => void; node?: React.ReactNode }) => {
  const [show, setShow] = useState(false);
  const index = useRef(-1);
  const close = useCallback(() => {
    hooks.splice(index.current, 1);
    removeEventListener();
    setShow(false);
    setTimeout(onDestroy, 200);
  }, [onDestroy]);
  useEffect(() => {
    setShow(true);
    index.current = hooks.push(close) - 1;
  }, [close]);
  return (
    ...
  );
};

这是我修改后的代码,此时 ESlint 警告消失了
另外,我通过你给我提供的信息发现
useCallbackuseEffect 其执行与否取决于依赖数组的变化
例如在 close - useCallback 中我使用了 onDestroy,那么依赖数组就需要加入 onDestroyuseEffect 同理
而在这之前,函数 close 的代码为 const close = () => { ... } ,我将其加入至 useEffect 的依赖数组中,渲染会导致 close 的重新创建,也就触发了 useEffect 的执行,形成了死循环
我说得对吗?如果不对请帮我纠正

你说得很对。下面是我对你刚才说的话的一点补充。
useCallbackuseEffect 确实根据其依赖列表中提供的值的变化来决定它们的执行。useCallback 在依赖列表中的任何值变化时都会返回一个新的函数实例,而 useEffect 则会在其依赖列表中的任何值变化时重新运行其副作用函数。
• 你正确地指出,当 close 函数是一个新创建的函数,每次组件重新渲染时,都会创建一个新的 close 函数实例。这就是为什么如果你将 close 函数直接包含在 useEffect 的依赖列表中,会导致 useEffect 在每次渲染时都运行,从而形成循环。
• 正如你所说,当定义一个函数组件内的函数(如 close),每次组件渲染时都会重新创建该函数。这就是为什么我们需要使用 useCallback 来确保我们能获取到一个在多次渲染间持久的函数引用。useCallback 会返回一个缓存版本的函数,只有当 useCallback 的依赖列表中的任何值发生变化时,这个函数才会更新。
因此,在你的最新的代码示例中,close 的定义使用了 useCallback 来确保只有当 onDestroy 属性变化时,close 函数才更新,这避免了无限循环问题,并符合 ESLint 的 react-hooks/exhaustive-deps 规则。

消息盒子
# 您需要首次评论以获取消息 #
# 您需要首次评论以获取消息 #

只显示最新10条未读和已读信息