👊 【React Day 1】Toast 组件实现

    136

🅰️ About

今天花了一天时间在看 React 相关的内容,配合着 ChatGPT 以及文档试着写了一个 Toast 组件

so,大概就是这样,一个十分常见的消息提示组件
关于 Framer Motion,以及 React 的项目结构,我参考了 cali 的 Next.js 博客
https://github.com/CaliCastle/cali.so

Source Code

需要依赖

  • framer-motion
  • tailwindcss
"use client";

import React, { useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { AnimatePresence, motion } from "framer-motion";
import EventEmitter from "events";

enum ToastType {
  "error" = "#f87171",
  "success" = "#34d399",
  "info" = "#94a3b8",
  "warning" = "#fbbf24",
}

type ToastDefine = {
  id?: string;
  content?: string;
  duration?: number;
  type: keyof typeof ToastType | string;
  color?: string;
  borderColor?: string;
  bgColor?: string;
};

class ToastManager {
  root: HTMLElement | null;
  eventEmitter: EventEmitter;
  onDestroy: (() => void) | null = null;

  constructor(onMounted: (eventEmitter: EventEmitter) => void, onDestroy: () => void) {
    this.eventEmitter = new EventEmitter();
    this.root = document.createElement("div");
    document.createElement("div");
    document.body.appendChild(this.root);
    createRoot(this.root).render(
      <ToastProvider
        onMounted={() => {
          onMounted(this.eventEmitter);
        }}
        destroy={this.destroy}
        eventEmitter={this.eventEmitter}
      />,
    );
    this.onDestroy = onDestroy;
  }

  destroy = () => {
    this.onDestroy && this.onDestroy();
    setTimeout(() => {
      this.root?.remove();
      this.root = null;
    }, 200);
  };
}

function ToastProvider({
  onMounted,
  destroy,
  eventEmitter,
}: {
  onMounted: () => void;
  destroy: () => void;
  eventEmitter: EventEmitter;
}) {
  const [toasts, setToasts] = useState<ToastDefine[]>([]);

  const showToastAction = (message: ToastDefine) => {
    message.id = String(Math.random());
    setToasts(oldToasts => [...oldToasts, message]);
    setTimeout(() => {
      setToasts(oldToasts => {
        if (oldToasts.length === 1) destroy();
        return oldToasts.filter(toast => toast.id !== message.id);
      });
    }, message.duration || 2000);
  };

  useEffect(() => {
    eventEmitter.on("showToast", showToastAction);
    if (onMounted) onMounted();
    return () => {
      eventEmitter.off("showToast", showToastAction);
    };
  }, []);
  return (
    <div className="fixed right-[20px] top-[20px] flex flex-col gap-[10px]">
      <AnimatePresence>
        {toasts.map(toast => (
          <Toast key={toast.id} msg={toast} />
        ))}
      </AnimatePresence>
    </div>
  );
}

const Toast = ({ msg }: { msg: ToastDefine }) => {
  const color = getColor(msg.type);
  const item: ToastDefine = {
    ...msg,
    color,
    borderColor: color,
    bgColor: `${color}20`,
  };

  return (
    <motion.div
      layout="position"
      initial={{ opacity: 0, scale: 0.5, x: -60 }}
      animate={{ opacity: 1, scale: 1, x: 0 }}
      exit={{ opacity: 0, scale: 0.5, x: -60 }}
      transition={{ duration: 0.2 }}
    >
      <div
        className="bg-white rounded-md min-w-[200px] p-[10px] text-[13px] border-solid border"
        style={{ backgroundColor: item.bgColor, color: item.color, borderColor: item.borderColor }}
      >
        {msg.content}
      </div>
    </motion.div>
  );
};

const getColor = (key: string) => {
  let color = ToastType[key as keyof typeof ToastType];
  if (color) return color;
  return key;
};

let toastInstance: ToastManager | null = null;

function show(msg: ToastDefine | string) {
  let message: ToastDefine = {
    duration: 2000,
    type: "info",
  };
  if (typeof msg === "string")
    message = {
      content: msg,
      ...message,
    };
  else message = { ...message, ...msg };

  if (!toastInstance) {
    toastInstance = new ToastManager(
      eventEmitter => {
        eventEmitter.emit("showToast", message);
      },
      () => {
        toastInstance = null;
      },
    );
  } else toastInstance.eventEmitter.emit("showToast", message);
}

export default { show };

调用

import Toast from "@/components/Toast";

Toast.show({
    content: "2333",
    type: "info",
});

关于 useEffect

也就是说,useEffect 的执行取决于第二个参数对吗?如果为 [] 则只会在挂载和卸载时执行,假如给定一个可变值,那么值的变化就会导致 useEffect 执行,是这样理解吗?

是的,你理解得完全正确。useEffect 的执行确实取决于它的第二个参数,也就是依赖数组。
•如果依赖数组为空([]),那么 useEffect 中的副作用函数只会在组件首次渲染(挂载)后和卸载前执行。它等效于 componentDidMountcomponentWillUnmount 生命周期方法。
•如果依赖数组包含一些变量,那么 useEffect 中的副作用函数将在组件首次渲染后,以及任何依赖项改变时执行。它等效于 componentDidMountcomponentDidUpdate 生命周期方法。
•如果不提供依赖数组,那么 useEffect 中的副作用函数将在组件首次渲染后和每次更新后(无论何时调用 render)执行。
所以,useEffect 可以根据需要,模拟类组件中的各种生命周期方法。

提出这个问题的缘由是因为起初想通过 useEffect(() => void, [toasts]) 来监听 toasts 值的改变(toasts 为空时清除容器divtoastInstance),但是这样也导致了函数 onMounted 的反复执行

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

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