👊 【React Day 2】Dialog 组件实现
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 警告消失了
另外,我通过你给我提供的信息发现
useCallback
、useEffect
其执行与否取决于依赖数组的变化
例如在close - useCallback
中我使用了onDestroy
,那么依赖数组就需要加入onDestroy
,useEffect
同理
而在这之前,函数close
的代码为const close = () => { ... }
,我将其加入至useEffect
的依赖数组中,渲染会导致close
的重新创建,也就触发了useEffect
的执行,形成了死循环
我说得对吗?如果不对请帮我纠正你说得很对。下面是我对你刚才说的话的一点补充。
•useCallback
和useEffect
确实根据其依赖列表中提供的值的变化来决定它们的执行。useCallback
在依赖列表中的任何值变化时都会返回一个新的函数实例,而useEffect
则会在其依赖列表中的任何值变化时重新运行其副作用函数。
• 你正确地指出,当close
函数是一个新创建的函数,每次组件重新渲染时,都会创建一个新的close
函数实例。这就是为什么如果你将close
函数直接包含在useEffect
的依赖列表中,会导致useEffect
在每次渲染时都运行,从而形成循环。
• 正如你所说,当定义一个函数组件内的函数(如close
),每次组件渲染时都会重新创建该函数。这就是为什么我们需要使用useCallback
来确保我们能获取到一个在多次渲染间持久的函数引用。useCallback
会返回一个缓存版本的函数,只有当useCallback
的依赖列表中的任何值发生变化时,这个函数才会更新。
因此,在你的最新的代码示例中,close
的定义使用了useCallback
来确保只有当onDestroy
属性变化时,close
函数才更新,这避免了无限循环问题,并符合 ESLint 的react-hooks/exhaustive-deps
规则。
空空如也!