首頁 > web前端 > js教程 > 存在主義的 React 問題和完美的模態對話框

存在主義的 React 問題和完美的模態對話框

Patricia Arquette
發布: 2025-01-03 03:44:41
原創
649 人瀏覽過

Existential React questions and a perfect Modal Dialog

你認為React中最複雜的事情是什麼?重新渲染?情境?門戶網站?並行?

不。

React 最困難的部分是它周圍的一切非 React。 「上面列出的那些東西是如何運作的?」這個問題的答案很簡單:只需遵循演算法並做筆記即可。結果將是確定的並且始終相同(如果您正確追蹤)。這只是科學和事實。

但是「什麼讓元件呢?」或「實作…(某事)的正確方法是什麼?」甚至「我應該使用函式庫還是建立自己的解決方案?」這裡唯一正確的答案是「這取決於情況」。它恰好是最沒有幫助的一個。

我想為新文章找到比這更好的東西。但由於這些類型的問題不可能有簡單的答案和通用的解決方案,因此這篇文章更多是我的思考過程的演練,而不是「這就是答案,永遠這樣做」。希望它仍然有用。

那麼,如何將功能從想法轉變為可投入生產的解決方案呢?讓我們嘗試實作一個簡單的模態對話框並看看。那有什麼可能是複雜的呢? ?

第 1 步:從最簡單的解決方案開始

讓我們從有時被稱為「尖峰」的東西開始 - 最簡單的實現,可以幫助探索潛在的解決方案並收集進一步的需求。我知道我正在實現一個模式對話框。假設我有一個像這樣的漂亮設計:

Existential React questions and a perfect Modal Dialog

對話方塊基本上是螢幕上的元素,當點擊按鈕之類的內容時會出現該元素。這正是我要開始的地方。

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <div className="dialog">some content</div>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

狀態,一個監聽點擊的按鈕,以及當狀態為 true 時顯示的未來對話框。對話框也應該有一個“關閉”操作:

<button
  className="close-button"
  onClick={() => setIsOpen(false)}
>
  Close
</button>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

它還有一個「背景」 - 一個可點擊的半透明 div,覆蓋內容並在點擊時觸發模式消失。

<div
  className="backdrop"
  onClick={() => setIsOpen(false)}
></div>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

大家在一起:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <>
          <div
            className="backdrop"
            onClick={() => setIsOpen(false)}
          ></div>
          <div className="dialog">
            <button
              className="close-button"
              onClick={() => setIsOpen(false)}
            >
              Close
            </button>
          </div>
        </>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製

我通常也會儘早添加合適的樣式。看到我正在實現的功能以與預期相同的外觀出現在螢幕上,這有助於我思考。另外,它還可以通知功能的佈局,這正是此對話方塊將發生的情況。

讓我們快速為背景添加 CSS - 它沒什麼特別的,只是 div 上的半透明背景,位置固定:佔據整個螢幕:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <div className="dialog">some content</div>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

該對話框稍微有趣一些,因為它需要放置在螢幕中間。當然,CSS 中有 1001 種方法可以實現這一目標,但我最喜歡的也可能是最簡單的一種是:

<button
  className="close-button"
  onClick={() => setIsOpen(false)}
>
  Close
</button>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

我們使用「固定」位置來擺脫佈局約束,新增 50% 的左側和頂部以將 div 移動到中間位置,然後將其變換回 50%。 left 和 top 將相對於螢幕進行計算,變換將相對於 div 本身的寬度/高度,因此,無論其寬度或螢幕寬度如何,它都會出現在中間。

此步驟中 CSS 的最後一點是正確設定對話方塊本身和「關閉」按鈕的樣式。這裡就不複製貼上了,實際的樣式並不重要,請看一下例子:

第二步:停下來,提出問題並思考

現在我已經粗略地實現了該功能,是時候讓它變得「真實」了。為此,我們需要詳細了解我們到底要解決什麼問題以及為誰解決問題。從技術上講,我們應該明白編碼任何東西之前,所以很多時候,這一步應該是第1步

此對話框是否是原型的一部分,需要盡快實施,向投資者展示一次,然後不再使用?或者它可能是您要在 npm 和開源上發布的通用庫的一部分?或者它可能是您的 5,000 人組織將使用的設計系統的一部分?或者它只是您的 3 人小型團隊的內部工具的一部分,僅此而已?或者,也許您在 TikTok 等公司工作,而此對話方塊將成為僅在行動裝置上可用的網路應用程式的一部分?或者您可能在一家只為政府編寫應用程式的機構工作?

回答這些問題可以決定下一步編碼的方向。

如果只是一個原型,用一次,可能就已經夠了。

如果它要作為庫的一部分開源,它需要有一個非常好的通用 API,世界上任何開發人員都可以使用和理解,大量的測試和良好的文檔。

作為 5,000 人組織的設計系統一部分的對話方塊需要遵守組織的設計準則,並且可能會限制將哪些外部依賴項帶入儲存庫。因此,您可能需要從頭開始實作許多事情,而不是執行 npm install new-fancy-tool。

為政府建立的機構的對話可能需要成為宇宙中最容易訪問且符合法規的對話。否則,該機構可能會失去政府合約並破產。

等等等等。

出於本文的目的,我們假設該對話框是現有大型商業網站當前正在進行的全新重新設計的一部分,該網站每天有來自世界各地的數千名用戶。重新設計正在進行中,我得到的唯一帶有對話框的設計是這樣的:

Existential React questions and a perfect Modal Dialog

剩下的稍後再說,設計師們都忙不過來了。此外,我是負責重新設計和維護網站的永久團隊的一員,而不是為單一專案僱用的外部承包商。

在這種情況下,僅憑這張圖片並了解我們公司的目標就可以為我提供足夠的資訊來做出合理的假設並實現 90% 的對話。剩下的10%可以稍後再微調。

這些是我根據上述資訊可以做出的假設:

  • 現有網站每天有來自世界各地的數千名用戶,因此我需要確保該對話框至少可以在大螢幕和行動螢幕以及不同的瀏覽器上運行。理想情況下,我需要檢查現有分析才能絕對確定,但這是一個非常安全的選擇。

  • 不只一位開發人員正在為此編寫程式碼,而且程式碼將保留下來。網站規模很大,已經擁有數千名用戶;對投資者來說,這不是一個快速的原型。所以,我需要確保程式碼可讀,API有意義,可用且可維護,並且沒有明顯的腳槍。

  • 公司關心其形象和網站的品質 - 否則,他們為什麼要重新設計? (我們假設這裡有積極的意圖?)。這意味著需要達到一定的品質水平,我需要提前思考並預測常見場景和邊緣情況,即使它們還不是目前設計的一部分。

  • 許多使用者可能意味著並非所有人都專門使用滑鼠與網站互動。該對話框還必須可以透過鍵盤互動甚至螢幕閱讀器等輔助技術來使用。

  • 現有的大型程式碼庫(記住,這是重新設計!)意味著我可以為此功能帶來的外部依賴項可能存在限制。任何外部依賴都是有代價的,尤其是在大型和舊的程式碼庫中。出於本文的目的,我們假設我可以使用外部庫,但我需要對此有一個很好的理由。

  • 最後,更多的設計即將到來,所以我需要從設計和用戶的角度預測它會走向何方,並確保程式碼可以儘早處理它。

第 3 步:固化模態對話框 API

現在我知道了需求並有了合理的猜測,我可以製作實際的對話框組件了。首先,從這段程式碼來看:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <div className="dialog">some content</div>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

我絕對需要將對話框部分提取到可重複使用元件中 - 將有大量基於對話框的功能需要實現。

<button
  className="close-button"
  onClick={() => setIsOpen(false)}
>
  Close
</button>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

對話方塊將有一個 onClose 屬性 - 當按一下「關閉」按鈕或背景時,它將通知父元件。然後,父元件仍將具有狀態並呈現對話框,如下所示:

<div
  className="backdrop"
  onClick={() => setIsOpen(false)}
></div>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

現在,讓我們再次看看設計並更多地考慮對話框:

Existential React questions and a perfect Modal Dialog

對話框中顯然會有一些帶有操作按鈕的“頁腳”部分。這些按鈕很可能會有很多變化 - 一個、兩個、三個、左對齊、右對齊、中間有空格等等。此外,此對話框沒有 標題 ,但是它非常非常有可能具有某些標題的對話框是一種非常常見的模式。這裡絕對會有一個內容區域,其中包含完全隨機的內容 - 從確認文本到表格,再到互動體驗,再到沒有人閱讀的很長的“條款和條件”可滾動文本。

最後是尺寸。設計中的對話框很小,只是一個確認對話框。大表格或長文不適合那裡。因此,考慮到我們在步驟 2 中收集的信息,可以非常安全地假設對話框的大小需要更改。此時,考慮到設計師可能有設計指南,我們可以假設對話方塊有三種變體:「小」、「中」和「大」。

所有這些意味著我們需要在 ModalDialog 上有 props:頁腳和 header 將只是接受 ReactNode 的常規 props,大小將只是字符串的聯合,而內容區域作為主要部分將進入孩子們:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <>
          <div
            className="backdrop"
            onClick={() => setIsOpen(false)}
          ></div>
          <div className="dialog">
            <button
              className="close-button"
              onClick={() => setIsOpen(false)}
            >
              Close
            </button>
          </div>
        </>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製

我們將使用來自道具的附加 className 來控制對話框的大小。但在現實生活中,它將很大程度上取決於儲存庫中使用的樣式解決方案。

然而,在這個變體中,對話框太靈活了 - 幾乎任何東西都可以去任何地方。例如,在頁腳中,大多數時候,我們只需要一兩個按鈕,僅此而已。這些按鈕必須一致地排列在整個網站的各處。我們需要一個包裝器來對齊它們:

.backdrop {
  background: rgba(0, 0, 0, 0.3);
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
登入後複製
登入後複製

與內容相同 - 至少,它需要一些周圍的填充和滾動能力。標題可能需要一些文字樣式。於是版面就變成這樣:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <div className="dialog">some content</div>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

但不幸的是,我們無法保證這一點。在某些時候,很可能有人希望在頁腳中添加按鈕以外的更多內容。或某些對話方塊需要在已售背景上有標題。或者有時,內容不需要填充。

我在這裡要指出的是,有一天我們需要能夠設計頁首/內容/頁尾部分的樣式。而且可能比預期早。

當然,我們可以只使用 props 傳遞該配置,並使用 headerClassName、contentClassName 和 footerClassName 等 props。實際上,對於某些情況來說,這可能沒問題。但對於像重新設計的漂亮對話框這樣的東西,我們可以做得更好。

解決這個問題的一個非常巧妙的方法是將我們的頁眉/內容/頁腳提取到它們自己的組件中,如下所示:

<button
  className="close-button"
  onClick={() => setIsOpen(false)}
>
  Close
</button>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

並將 ModalDialog 程式碼還原為沒有包裝器的程式碼:

<div
  className="backdrop"
  onClick={() => setIsOpen(false)}
></div>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

這樣,在父應用程式中,如果我想要對話框部分的預設設計,我會使用這些微小的元件:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <>
          <div
            className="backdrop"
            onClick={() => setIsOpen(false)}
          ></div>
          <div className="dialog">
            <button
              className="close-button"
              onClick={() => setIsOpen(false)}
            >
              Close
            </button>
          </div>
        </>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製

如果我想要完全自訂的東西,我會實作一個具有自己的自訂樣式的新元件,而不會弄亂 ModalDialog 本身:

.backdrop {
  background: rgba(0, 0, 0, 0.3);
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
登入後複製
登入後複製

就此而言,我甚至不再需要頁首和頁尾道具。我可以將 DialogHeader 和 DialogFooter 傳遞給子級,進一步簡化 ModalDialog,並擁有更好的 API,具有相同程度的靈活性,同時在各處都有一致的設計。

父組件將如下圖所示:

.dialog {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
登入後複製

對話框的 API 將如下所示:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <>
          <div
            className="backdrop"
            onClick={() => setIsOpen(false)}
          ></div>
          <div className="dialog">
            <button
              className="close-button"
              onClick={() => setIsOpen(false)}
            >
              Close
            </button>
          </div>
        </>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製

到目前為止我對此非常滿意。它足夠靈活,可以以設計可能需要的任何方式進行擴展,但它也足夠清晰和合理,可以輕鬆地在整個應用程式中實現一致的 UI。

這是可以使用的實例:

第四步:效能和重新渲染

現在 Modal 的 API 已經夠好了,是時候解決我實現的明顯的腳槍問題了。如果你讀夠了我的文章,你可能已經大聲尖叫,「你在做什麼???重新渲染!!」最後十分鐘?當然,你是對的:

const ModalDialog = ({ onClose }) => {
  return (
    <>
      <div className="backdrop" onClick={onClose}></div>
      <div className="dialog">
        <button className="close-button" onClick={onClose}>
          Close
        </button>
      </div>
    </>
  );
};
登入後複製

這裡的Page組件是有狀態的。每次模式開啟或關閉時,狀態都會發生變化,並且會導致整個元件及其內部所有內容的重新渲染。是的,“過早的優化是萬惡之源”,是的,在實際測量性能之前不要優化性能,在這種情況下,我們可以安全地忽略傳統觀點。

有兩個原因。首先,我知道一個事實是,整個應用程式中會分散很多模式。這不是一個沒有人會使用的一次性隱藏功能。因此,有人將狀態放置在不應該使用這樣的 API 的地方的可能性非常高。其次,從一開始就不需要花費太多時間和精力來防止重新渲染問題的發生。只要1分鐘的努力,我們根本不需要考慮這裡的效能。

我們需要做的就是封裝狀態並引入「不受控組件」的想法:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <div className="dialog">some content</div>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

BaseModalDialog 與我們之前的對話框完全相同,我只是重命名了它。

然後傳遞一個應該觸發對話框的元件作為觸發道具:

<button
  className="close-button"
  onClick={() => setIsOpen(false)}
>
  Close
</button>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

頁面元件將如下所示:

<div
  className="backdrop"
  onClick={() => setIsOpen(false)}
></div>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

頁面內不再有狀態,不再有潛在危險的重新渲染。

這樣的 API 應該涵蓋 95% 的用例,因為大多數時候,使用者需要點擊某些內容才能顯示對話框。在極少數情況下,當對話方塊需要獨立顯示時,例如,在捷徑上或作為入門的一部分,我仍然可以使用 BaseModalDialog 並手動處理狀態。

第 5 步:處理邊緣情況和可訪問性

從 React 的角度來看,ModalDialog 元件的 API 非常可靠,但工作還遠遠沒有完成。考慮到我在步驟 2 中收集的必備條件,我還需要解決更多問題。

問題 1:我將觸發器包裝到一個額外的跨度中 - 在某些情況下,這可能會破壞頁面的佈局。我需要以某種方式去除包裝紙。

問題 2:如果我在建立新堆疊上下文的元素內渲染對話框,則模式將出現在某些元素下方。我需要在 Portal 內渲染它,而不是像我現在一樣直接在佈局內渲染。

問題 3:目前鍵盤存取非常糟糕。當正確實現的模式對話方塊打開時,焦點應該跳到裡面。當它關閉時 - 焦點應該會回到觸發對話方塊的元素。當對話方塊打開時,焦點應該被「困」在裡面,而外面的元素不應該是可聚焦的。按 ESC 按鈕應關閉該對話方塊。目前這些都還沒實現。

問題 1 和 2 有點煩人,但可以相對快速地解決。然而,手動完成第 3 個問題是一件非常痛苦的事情。另外,這肯定是一個已解決的問題 - 每個地方的每個對話框都需要此功能。

「我自己做的巨大痛苦」「看起來肯定是一個已解決的問題」的組合是我尋找現有庫的地方。

考慮到我已經完成的所有前期工作,現在選擇合適的就很容易了。

我可以使用任何現有的 UI 元件庫,例如 Ant Design 或 Material UI,並使用其中的對話框。但如果重新設計不使用它們,將他們的設計調整為我需要的,會帶來比他們解決的更多的痛苦。所以對於這種情況,立即否定。

我可以使用「無頭」UI 函式庫之一,例如 Radix 或 React Aria。它們實現了狀態和觸發器等功能以及所有可訪問性,但將設計留給了消費者。在查看他們的 API 時,我需要仔細檢查它們是否允許我控制對話框的狀態,如果我確實需要它來手動觸發對話框(他們確實這樣做)。

如果因為某些原因,我無法使用無頭函式庫,我至少會嘗試使用處理焦點陷阱功能的函式庫。

為了本文的目的,我們假設我可以帶任何我想要的函式庫。在這種情況下,我將使用 Radix - 它非常易於使用,並且對話框的 API 看起來與我已經實現的非常相似,因此重構應該是輕而易舉的。

我們需要稍微改變一下對話框本身的 API:

export default function Page() {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <button onClick={() => setIsOpen(true)}>
        Click me
      </button>
      {isOpen ? (
        <div className="dialog">some content</div>
      ) : null}
    </>
  );
}
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

跟我以前的幾乎一樣。只是,我沒有使用 div,而是使用 Radix 原語。

不受控制的對話框用法根本沒有改變:

<button
  className="close-button"
  onClick={() => setIsOpen(false)}
>
  Close
</button>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

受控對話框略有變化 - 我需要將道具傳遞給它而不是條件渲染:

<div
  className="backdrop"
  onClick={() => setIsOpen(false)}
></div>
登入後複製
登入後複製
登入後複製
登入後複製
登入後複製

查看下面的範例並嘗試使用鍵盤進行導航。一切都按照我的需要進行,這有多酷?

作為獎勵,Radix 還可以處理 Portal 問題,並且它不會將觸發器包裝在一個跨度中。我不再需要解決邊緣情況,所以我可以繼續最後一步。

第6步:最後拋光

該功能還沒完成! ?該對話框現在看起來和感覺都相當可靠,因此現階段我不會對其實現進行任何重大更改。但對於我正在解決的用例,它仍然需要一些東西才能被認為是「完美」對話框。

One:設計師要求我做的第一件事(如果他們還沒做的話)就是在對話框打開時添加一個微妙的動畫。需要預見它並記住如何在 React 中製作動畫。

兩個:我需要向對話框添加最大寬度和最大高度,以便在小螢幕上它仍然看起來不錯。想想它在大螢幕上的樣子。

:我需要與設計師討論對話框在行動裝置上的行為方式。他們很可能會要求我將其做成一個滑入式面板,無論對話框的大小如何,它都會佔據大部分螢幕。

四個:我需要至少引入 DialogTitle 和 DialogDescription 元件 - Radix 會要求將它們用於輔助功能。

:測驗!該對話方塊將保留下來並由其他人維護,因此在這種情況下測試幾乎是強制性的。

也許還有很多我現在忘記的小事情,稍後會出現。更不用說實現對話框內容的實際設計了。

還有一些想法

如果將上面的“對話框”替換為“SomeNewFeature”,這或多或少是我用來實現幾乎所有新功能的演算法。

解決方案的快速「峰值」→收集功能需求→使其工作→使其高效能→使其完整→使其完美。

對於像實際對話框這樣的東西,我已經實現了數百次,我會在 10 秒內在腦海中完成第一步,然後立即從步驟 2 開始。

對於非常複雜和未知的事情,第 1 步可能會更長,並且涉及立即探索不同的解決方案和函式庫。

一些不完全未知的東西,只是“我們需要做的常規功能”,可能會跳過步驟 1,因為可能沒有什麼可探索的。

很多時候,尤其是在「敏捷」環境中,它更像是螺旋而不是直線,需求是增量提供的並且經常變化,我們會定期返回前兩個步驟。


希望這類文章有用! ??如果您想要更多這樣的內容或更喜歡通常的“事情如何運作”的內容,請告訴我。

並期待聽到你們所有人的腦中這個過程有何不同?


最初發佈於 https://www.developerway.com。網站還有更多這樣的文章嗎?

看看《Advanced React》一書,將您的 React 知識提升到一個新的水平。

訂閱電子報、在 LinkedIn 上聯繫或在 Twitter 上關注,以便在下一篇文章發佈時立即收到通知。


順便說一句,最後一件事:如果您很快就要開始一個新項目,並且沒有設計師,也沒有時間來完善所描述的設計體驗- 我最近花了幾個小時(又幾個小時)來實作一個新的專案本例的UI 元件庫。它具有可複製貼上的組件和常見模式、Radix 和 Tailwind、深色模式、可訪問性以及開箱即用的行動支援。包括上面完美的模態對話框! ?

試試看:https://www.buckets-ui.com/

Existential React questions and a perfect Modal Dialog

以上是存在主義的 React 問題和完美的模態對話框的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板