React.createPortal 实现一个简单的 message 组件

535 阅读1分钟

日常开发中有全局 message 组件的需求.

  • 全局使用
  • 样式隔离(避免其他组件样式影响 message 组件样式)

Context

Context 全局提供 message 方法

Portal

Portal 将子节点渲染到 body 避免与父节点发生样式冲突

实现

MessageContext

// 省略引用
// message 类型
type MessageType = 'success' | 'error' | 'info'

// message 配置
interface MessageOption {
  type: MessageType
  duration: number
}

// success... 方法
type MessageMethod = (
  content: string,
  option?: Partial<Omit<MessageOption, 'type'>>
) => void

// context props
interface MessageProps {
  success: MessageMethod
  error: MessageMethod
  info: MessageMethod
}

const MessageContext = React.createContext<MessageProps>({
  success: () => {},
  error: () => {},
  info: () => {},
})

// 方便其他组件使用
export function useMessage() {
  return React.useContext(MessageContext)
}

MessageContent

message 显示组件,根据需求调整

// message 类型图标
const MessageTypeIcons: { [props in MessageType]: ReactNode } = {
  success: <IcBaselineCheckCircle className="w-6 h-6 text-teal-600" />,
  error: <IcBaselineError className="w-6 h-6 text-yellow-500" />,
  info: <IcBaselineInfo className="w-6 h-6 text-gray-500" />,
}

interface Props {
  message: string
  type: MessageType
}

const MessageContent: React.FC<Props> = ({ message, type }) => {
  return (
    <div
      role="alert"
      className="fixed top-8 z-50 w-fit min-w-[200px] left-4 right-4 ml-auto mr-auto rounded-xl border bg-white border-gray-100 p-4 shadow-xl dark:border-gray-800 dark:bg-gray-900"
    >
      <div className="flex items-start gap-4">
        <span>{MessageTypeIcons[type]}</span>
        <div className="flex-1">
          <strong className="block text-center font-medium text-gray-900 dark:text-white">
            {message}
          </strong>
        </div>
      </div>
    </div>
  )
}

安利图标网站 Icônes (icones.js.org)

这里使用 Tailwind CSS 实现样式,核心思想是 fixed 定位,居中显示,根据个人喜好实现就好.

.message{
    position: fixed;
    width: fit-content;
    top: 2vh;
    left: 1rem;
    right: 1rem;
    margin-left: auto;
    margin-right: auto;
}

MessageProvider

const DEFALUT_MESSAGE_OPTION: MessageOption = {
  type: 'info',
  duration: 2000,
}

export const MessageProvider: React.FC<PropsWithChildren> = ({ children }) => {
  const [showModal, setShowModal] = useState(false)
  const [msg, setMsg] = useState('')
  const [messageOption, setMessageOption] = useState<MessageOption>(DEFALUT_MESSAGE_OPTION)

  useEffect(() => {
    if (showModal) {
      setTimeout(() => {
        setShowModal(false)
      }, messageOption.duration)
    }
  }, [messageOption.duration, showModal])

  // 对应type的方法
  const messageMethod = useCallback(
    (type: MessageType) =>
      (content: string, option?: Partial<Omit<MessageOption, 'type'>>) => {
        setShowModal(true)
        setMsg(content)
        setMessageOption((opt) => ({ ...opt, ...option, type }))
      },
    []
  )

  return (
    <MessageContext.Provider
      value={{
        success: messageMethod('success'),
        error: messageMethod('error'),
        info: messageMethod('info'),
      }}
    >
      {showModal &&
        ReactDOM.createPortal(
          <MessageContent message={msg} type={messageOption.type} />,
          document.body
        )}
      {children}
    </MessageContext.Provider>
  )
}

使用

MessageProvider 包裹 需要使用的组件

 <MessageProvider>
    <App />
 </MessageProvider>

useMessage 调用方法

const MessagePagae: React.FC = () => {
    const message = useMessage()
    return (
        <div>
             <button onClick={() => message.success('success')}>
                success 
             </button>
             <button onClick={() => message.error('error')}>
                error 
             </button>
             <button onClick={() => message.info('info')}>
                info 
             </button>
        </div>
    )
}

这样就实现了一个 message 组件基本需求,还有许多地方可以完善

  • 美化样式,添加动画
  • 添加配置和手动关闭方法
  • 优化同时显示多个消息
  • ...

资源