一次Toast元件引發的思考

xiaoTuiMao發表於2019-12-07

背景

最近組內有一個新專案,需要用的Toast這樣一個元件。心裡想,這樣的元件,還不是分分鐘就搞定呀。然後一頭砸進去了開始寫。

如何做

toast 的展示與否,跟展示什麼我都交給父元件去做,Toast本身只管展示就可以了,內部需要任何別的邏輯 第一版程式碼如下

import React from 'react';
import styles from './style.module.css';


const Toast = ({ content, showToast }) => {
  if (!showToast) {
    return null;
  }

  return <div className={styles.container}>{content}</div>;
};

export default Toast;
複製程式碼

然後再呼叫的地方,發現如果我需要使用Toast,那麼我使用的地方都得引入一下,這顯然很不友好。 然後我就將控制Toast的展示與否交由最外層的App元件去做, 如果那裡需要使用Toast,那麼就將handleShowToast 傳遞下去就好了 程式碼如下

import React, { useState } from 'react';
import Toast from '@components/Toast';

const App = (props) => {
    const [showToast, setToastVisible] = useState(false);
    const [content, setToastContent] = useState('');
    const handleShowToast = (toastContent, delay) => {
        setToastVisible(true);
        setToastContent(toastContent);
        const timer = setTimeOut(() => {
            setToastVisible(false);
            setToastContent('');
        }, delay)
    }
    
    return (
        <div>
            <Toast showToast={showToast} content={content}/>
            <div>
                ...
            </div>
        </div>
    );
};

export default App;

複製程式碼

但是這樣用起來還是不舒服,如果頁面多了,那豈不是每一處頁面都需要這麼處理,如果層級比較深,那豈不是要一層一層的傳遞下去?

綜上所碰到的問題,我思考了一下我想要的Toast元件的樣子

  1. 只需頁面引入一次即可
  2. Toast 本身的邏輯自己處理,不需要呼叫方去管
  3. Toast 需要暴露一些 API,讓業務方去呼叫

整理完上面的問題,想起了觀察者模式的應用,我是不是可以,在 Toast 本身去訂閱幾個事件,然後當我想要處理跟Toast 相關邏輯的時候我再 emit 相關事件,事情是不是就變的簡單了呢?

首先我需要一個事件系統,這個事件系統需要滿足以下功能

  1. on 方法去訂閱事件
  2. off 去解除訂閱
  3. emit 方法去觸發事件
  4. 有一個 list 去儲存所有相關事件。 最終實現的 event 如下
interface EventD {
  list: any;
}

export default class Event implements EventD {
  list = new Map();

  on(event: string, callback: any) {
    if (!this.list.has(event)) {
      this.list.set(event, []);
    }
    this.list.get(event).push(callback);
    return this;
  }

  off(event: string) {
    this.list.delete(event);
    return this;
  }

  emit(event: string, ...args: any) {
    if (this.list.has(event)) {
      this.list.get(event).forEach((callback: any) => setTimeout(() => callback(...args), 0));
    }
  }
}

複製程式碼

有了事件系統,我們再去改造以下我們的Toast

import React, { useEffect, useState } from 'react';

import Event from '@lib/event';
import styles from './style.module.css';

const event = new Event();

const Toast = () => {
  const [showToast, setToastVisible] = useState(false);
  const [content, setToastContent] = useState('');

  useEffect(() => {
    event.on('showToast', (toastContent: string, delay: number = 2000) => {
      setToastContent(toastContent);
      setToastVisible(true);
      const timer = setTimeout(() => {
        clearTimeout(timer);
        setToastVisible(false);
      }, delay);
    });
    return () => {
      event.off('showToast');
    };
  });

  if (!showToast) {
    return null;
  }

  return <div className={styles.container}>{content}</div>;
};

export default Toast;

export const showToast = (content: string, delay?: number) => event.emit('showToast', content, delay);
複製程式碼

這樣我們就可以在APP元件內引用一次即可,如果需要展示 toast 的時候,只需要呼叫一下 Toast 元件暴露的 showToast 方法即可。

總結

  1. 類似 Toast 這樣需要事件系統的元件還有很多,我們都可以往這樣的思維方式上面去改造
  2. 在寫 React 的時候不要總侷限於,父子間的一層一層的傳遞

相關文章