React秘籍:手把手教你优雅封装一个Toast组件

在这里插入图片描述

为什么使用 Toast 通知

  1. 即时反馈:Toast 提供即时的操作反馈给用户,让他们知道他们的动作是否成功。
  2. 不干扰用户体验:由于 Toast 会在短时间后自动消失,因此它不会过分打扰用户或中断用户当前的工作流程。
  3. 简洁明了:Toast 专注于传达简单直接的信息,帮助用户快速理解情况而无需深入了解详情。
  4. 定时消失:Toast 应该在几秒钟后自动消失。确保设置合理的显示时间,让用户有足够时间阅读消息。

步骤 1: 创建Context和Provider

首先,在toast.context.tsx文件中,创建了一个Context和对应的Provider。这是实现跨组件状态共享的关键。

创建Context
export const toastContext = createContext<ContextType>({} as ContextType);

这行代码通过调用createContext函数创建了一个新的React上下文(toastContext),它允许将Toast相关的数据和操作方法共享给所有子组件。

在React项目中使用createContext来创建一个新的上下文,就好比是在为组件间共享信息搭建一个内部群聊——无需复杂地逐层传递props,任何“加入”了该上下文的组件都可以直接接收并使用这些共享的信息。

定义Provider

在React中,React.FCReact.FunctionComponent的缩写,它是TypeScript中用于标注或定义一个函数式组件的类型。

export const ToastContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
    const [state, dispatch] = useReducer(reducer, initialState);

    /**
     * removeAlert函数用于移除指定id的提示信息。
     * @param id - 提示信息的唯一标识符
     */
     
    // 使用 useCallback 创建了一个记忆化的 removeAlert 函数,用于移除指定 ID 的 Toast 通知
    // 如果不使用 useCallback,每次组件重新渲染时,removeAlert 函数都会被重新创建,这会导致 Toast 通知无法正确移除。
    // removeAlert 函数只会在组件首次渲染时被创建一次,之后不会再重新创建
    
    const removeAlert = useCallback((id: string) => {
        dispatch({
            type: "REMOVE_TOAST_ALERT",
            payload: { id, title: "", message: "", type: "success" },
        });
    }, []);

    /**
     * setToastAlert函数用于设置提示信息。
     * @param data - 提示信息的数据
     * @param data.title - 提示信息的标题
     * @param data.type - 提示信息的类型,可选值为 "success" | "error" | "warning" | "info"
     * @param data.message - 提示信息的内容
     */
     
    //useCallback 是 React 提供的一个 Hook,它可以返回一个记忆化的版本的回调函数。
    // 若 removeAlert 发生变化,setToastAlert 也会重新创建,这样就可以确保每次调用 setToastAlert 时都使用最新的 removeAlert 函数。
    // 实际上,removeAlert 函数在组件首次渲染时就已经创建了,且不会再重新创建,但是setToastAlert 的定义依赖于 removeAlert
    //只有当依赖项(在本例中为removeAlert)发生变化时,这个回调函数才会被重新创建。
    
    const setToastAlert = useCallback(
        (data: {
            title: string;
            type?: "success" | "error" | "warning" | "info";
            message?: string;
        }) => {
            const id = uuid();
            const { title, type, message } = data;
            dispatch({
                type: "SET_TOAST_ALERT",
                payload: { id, title, message, type: type ?? "success" },
            });

            const timer = setTimeout(() => {
                removeAlert(id);
                clearTimeout(timer);
            }, 3000);
        },
        [removeAlert]
    );
    
    return (
    //使用了上下文(Context)API 的Provider组件。通过它将一些值传递给所有在此上下文中的消费者(consumer)组件
        <toastContext.Provider value={{ setToastAlert, removeAlert, alerts: state.toastAlerts }}>
            <ToastAlert />
            //结构赋值语法,用于从传入的props对象中提取名为children的属性
            {children}
         // 渲染内容
        </toastContext.Provider>
    );
};

ToastContextProvider组件内部,使用了useReducer来管理Toast通知的状态,并定义了两个操作函数:setToastAlertremoveAlert。这个组件返回一个Provider组件,该组件通过其value属性向下分发状态和操作函数给所有子组件。

在提供的代码片段中,removeAlert 函数通过 useCallback 创建,并且它的依赖数组是空的([])。这意味着,只有在组件首次渲染时,removeAlert 函数会被创建一次。在组件的后续重新渲染过程中,只要该组件没有被卸载,removeAlert 保持不变。

removeAlert 发生变化的情况

在这个特定例子中,由于 useCallback 的依赖数组是空的,所以在组件的正常生命周期内(从挂载到卸载),以下情况会导致 removeAlert 发生变化:

  • 组件重新挂载:如果该组件被卸载然后再次挂载(例如,在条件渲染或路由切换的情况下),React将重新执行包括 useCallback 在内的所有hooks。这将导致创建一个新的 removeAlert 函数实例。
为什么把 removeAlert 放入 setToastAlert 的依赖数组

尽管在这个例子中 removeAlert 的定义不依赖于组件状态或props变化而变化(因为其依赖数组为空),将 removeAlert 作为 setToastAlert 的依赖项看起来似乎是多余的。但是,这么做有两个潜在原因:

  1. 遵循React Hooks规则:React强烈建议所有外部作用域中引用的值,如果你在effect、callback等Hooks使用了它们,应该包含在依赖数组中。即使我们知道这个值不会变更,显式地列出依赖项可以提供更清晰的代码意图,并保持与eslint等工具检查规则的一致性。
  2. 代码可维护性:虽然当前 removeAlert 的实现不需要任何外部数据且不会改变,但未来可能会修改其逻辑以依赖于某些状态或props。此时,明确地将它作为依赖列出意味着你不需要回头修改已经写好的代码结构。这样可以减少未来改动带来潜在bug。

总之,在这个例子里,由于 removeAlert 使用了空数组作为其依赖项列表,表明它只会在组件首次渲染时创建一次,在组件生命周期内保持不变。把它作为其他 useCallback 或者 useEffect 的依赖是遵循React Hooks使用原则,并考虑到代码未来可能发生变化的预防性措施。

步骤 2: 管理状态逻辑

使用Reducer来管理添加和移除Toast通知的逻辑。

export const reducer: ReducerFunctionType = (state, action) => {
    const { type, payload } = action;
  
    switch (type) {
      case "SET_TOAST_ALERT":
        return {
          ...state,
          toastAlerts: [...(state.toastAlerts ?? []), payload],
        };
  
      case "REMOVE_TOAST_ALERT":
        return {
          ...state,
          toastAlerts: state.toastAlerts?.filter((toastAlert) => toastAlert.id !== payload.id),
        };
  
      default: {
        return state;
      }
    }
};

通过定义不同的action类型来处理不同的状态更新逻辑。例如,当action类型为"SET_TOAST_ALERT"时,将新的Toast添加到状态数组中;当action类型为"REMOVE_TOAST_ALERT"时,从数组中移除指定ID的Toast。

步骤 3: 封装操作函数

在Provider内部封装两个主要操作函数:设置新的Toast(setToastAlert),和移除已存在的Toast(removeAlert})。

const setToastAlert = useCallback(
        (data: {
            title: string;
            type?: "success" | "error" | "warning" | "info";
            message?: string;
        }) => {
             // useCallback的第一个参数是一个函数,这个函数包含了回调函数的主体逻辑。 
             /* 函数体 */ 
            const id = uuid();
            const { title, type, message } = data;
            dispatch({
                type: "SET_TOAST_ALERT",
                payload: { id, title, message, type: type ?? "success" },
            });
            const timer = setTimeout(() => {
                removeAlert(id);
                clearTimeout(timer);
            }, 3000);
        },[removeAlert]  // useCallback的第二个参数是一个依赖项数组。 
    );

这里使用了uuid生成唯一标识符,并通过dispatch发送对应action来更新状态。同时利用setTimeout实现自动移除功能。

dispatch 函数是 useReducer Hook 提供的一个核心机制,它允许你向 reducer 函数发送(或“分发”)action。这个过程是如何工作的,本质上是 React 内部对 useReducer Hook 的实现逻辑,但可以通过其行为来理解其运作原理。

理解 dispatch 函数

当你调用 dispatch 并传递一个 action 对象给它时,React 会将这个 action 对象作为参数调用你定义的 reducer 函数。根据你在 reducer 函数中对不同 action 类型所定义的处理逻辑,reducer 会返回一个新的状态对象。然后,React 接管并更新组件状态,使得 UI 可以根据最新状态重新渲染。

Action 对象

Action 对象至少包含一个 type 字段,它是一个字符串常量,用于在 reducer 中识别要执行什么操作。除 type 外,action 对象还可以包含其他数据字段,这些字段携带执行操作所需的额外信息或数据。

Reducer 函数

Reducer 函数形式为 (state, action) => newState,它接收当前状态和一个 action 作为参数,并根据 action 的类型返回更新后的新状态。这个函数必须是纯净的——即给定相同的输入(当前状态和action),总是返回相同的输出(新状态),而且不产生副作用。

如何做到的?

当你调用 dispatch(action) 时:

  1. 传递 Action:你传递给 dispatch 的 action 对象被送入 reducer 函数作为第二个参数。
  2. 计算新状态:reducer 根据接收到的 action 类型(以及可能的其他信息)来决定如何更新状态。它计算出一个新状态并返回。
  3. 更新状态:React 接收从 reducer 返回的新状态,并触发组件使用这个新状态重新渲染。

步骤 4: 创建自定义Hook

为了方便地在组件内部访问context提供的数据和方法,创建了自定义Hook useToast.js

const useToast = () => {
    const toastContextData = useContext(toastContext);
    return toastContextData;
};

通过调用React内置的useContext() Hook并传入创建的context对象(即 toastContext) 来获取context中当前提供的值。

步骤 5: 使用Toast组件

在需要显示Toast通知的地方,调用自定义Hook useToast() 来获取显示及隐藏通知所需的方法。

示例:显示成功消息
const { setToastAlert } = useToast();
setToastAlert({
  title: 'Login Successful',
  type: 'success'
});
渲染Toasts列表

在前面创建Provider时嵌入的 <ToastAlert /> 组件会负责渲染当前所有活跃Toasts。

const ToastAlerts = () => {
    const { alerts, removeAlert } = useToast();

    if (!alerts) return null;

    return (
        <div>
            {/* 显示每个alert */}
        </div>
    );
};

这里根据从context获取到的alerts数组来遍历渲染每个活跃的Toast。每个Toast都提供了一个关闭按钮来触发移除逻辑。

Toast组件实现

//ToastAlerts 组件用于显示多个提示信息的弹出框。
const ToastAlerts = () => {
  // 使用useToast Hook获取当前活跃的alerts数组和可用于移除特定alert的removeAlert函数。
  const { alerts, removeAlert } = useToast();

  // 如果alerts数组为空,则不渲染或返回null,即不显示任何内容。
  if (!alerts) return null;

  // 渲染包含所有活跃alerts的容器,设置为固定位置并且拥有足够空间容纳多个alerts。
  return (
    <div className="pointer-events-none fixed top-5 right-5 z-50 h-full w-80 space-y-5 overflow-hidden">
      {/* 遍历alerts数组,为每个alert生成一个包含消息内容和操作按钮的视图 */}
      {alerts.map((alert) => (
        // 每个alert都有一个唯一id作为key,使用绝对定位布局包含关闭按钮和信息展示区域
        <div className="relative overflow-hidden rounded-md text-white" key={alert.id}>
          {/* 关闭按钮,点击时调用removeAlert函数并传递当前alert的id以将其移除 */}
          <div className="absolute top-1 right-1">
            <button
              type="button"
              className="pointer-events-auto inline-flex rounded-md p-1.5 focus:outline-none focus:ring-2 focus:ring-offset-2"
              onClick={() => removeAlert(alert.id)}
            >
              {/* 屏幕阅读器文本 */}
              <span className="sr-only">Dismiss</span>
              {/* 显示一个X形状图标作为关闭按钮 */}
              <XMarkIcon className="h-5 w-5" aria-hidden="true" />
            </button>
          </div>
          {/* 根据alert类型决定背景颜色,并展示警告标题和可选消息内容 */}
          <div
            className={`px-2 py-4 ${
              alert.type === "success"
                ? "bg-[#06d6a0]"
                : alert.type === "error"
                ? "bg-[#ef476f]"
                : alert.type === "warning"
                ? "bg-[#e98601]"
                : "bg-[#1B9aaa]"
            }`}
          >
            {/* 标题和消息内容布局 */}
            <div className="flex items-center gap-x-3">
              {/* 根据alert类型显示相应图标 */}
              <div className="flex-shrink-0">
                {alert.type === "success" ? (
                  <CheckCircleIcon className="h-8 w-8" aria-hidden="true" />
                ) : alert.type === "error" ? (
                  <XCircleIcon className="h-8 w-8" />
                ) : alert.type === "warning" ? (
                  <ExclamationTriangleIcon className="h-8 w-8" aria-hidden="true" />
                ) : (
                  <InformationCircleIcon className="h-8 w-8" />
                )}
              </div>
              {/* 显示标题和消息内容,如果存在消息内容则附加显示 */}
              <div>
                <p className="font-semibold">{alert.title}</p>
                {alert.message && <p className="mt-1 text-xs">{alert.message}</p>}
              </div>
            </div>
          </div>
        </div>
      ))}
    </div>
  );
};
  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
以下是一个简单的React封装toast组件的示例: ```jsx import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; import './toast.css'; const Toast = ({ message, duration = 3000, onClose }) => { const [visible, setVisible] = useState(false); useEffect(() => { setVisible(true); const timer = setTimeout(() => { setVisible(false); onClose && onClose(); }, duration); return () => clearTimeout(timer); }, [duration, onClose]); return visible ? ( <div className="toast"> <div className="toast-message">{message}</div> </div> ) : null; }; const showToast = (message, duration = 3000, onClose) => { const div = document.createElement('div'); document.body.appendChild(div); ReactDOM.render( <Toast message={message} duration={duration} onClose={() => { ReactDOM.unmountComponentAtNode(div); onClose && onClose(); }} />, div ); }; export default showToast; ``` 这个组件包含一个`Toast`组件一个`showToast`函数。`Toast`组件接收`message`、`duration`和`onClose`作为属性,其中`message`表示要显示的消息,`duration`表示消息显示的时间(默认为3秒),`onClose`表示关闭消息时的回调函数。 `showToast`函数用于在页面上创建一个`Toast`组件并显示消息。它接收与`Toast`组件相同的属性,还有一个可选的`onClose`回调函数,用于在消息关闭时执行其他操作。 在使用时,可以像这样调用`showToast`函数: ```jsx import React from 'react'; import showToast from './toast'; const MyComponent = () => { const handleClick = () => { showToast('Hello, world!', 2000, () => console.log('Toast closed.')); }; return ( <button onClick={handleClick}>Show Toast</button> ); }; export default MyComponent; ``` 这个示例在点击按钮时显示一个消息框,显示2秒后关闭,并在关闭时输出一条消息到控制台。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值