React 中的複合組件
已發表: 2022-03-10複合組件可幫助開發人員構建更具表現力和靈活性的 API,以在組件內共享狀態和邏輯。 本教程解釋瞭如何借助 Context API 和 React 來使用這種高級模式構建組件來實現這一點。
注意:為了能夠繼續學習,您需要對 React 以及 Context API 的工作原理有基本的了解。
什麼是複合成分?
複合組件可以說是一種模式,它包含了一組組件的狀態和行為,但仍將其可變部分的渲染控制權交還給外部用戶。
從上面的定義中,注意關鍵字: state和behavior 。 這有助於我們理解複合組件處理狀態(即狀態如何在由作為組件父級的外部用戶包圍的組件中表現)。
複合組件的目標是為父子組件之間的通信提供更具表現力和靈活性的 API。
把它想像成 HTML 中的<select>
和<option>
標籤:
<select> <option value="volvo">Volvo</option> <option value="mercedes">Mercedes</option> <option value="audi">Audi</option> </select>
select
標籤與option
標籤一起使用,用於下拉菜單以選擇 HTML 中的項目。 這裡<select>
管理 UI 的狀態,然後<option>
元素配置<select>
應該如何工作。 React 中的複合組件用於構建聲明性 UI 組件,這有助於避免道具鑽探。
道具鑽孔正在將道具向下傳遞多個子組件。 這也是他們所說的“代碼氣味”。 prop Drill最糟糕的地方在於,當父組件重新渲染時,子組件也會重新渲染,從而對組件造成多米諾骨牌效應。 一個好的解決方案是使用我們稍後會研究的 React Context API。
在 React 中應用複合組件
本節解釋了我們可以在我們的應用程序中使用的包,它們採用了在 React 中構建組件的複合組件模式。 此示例是來自@reach
UI 包的Menu
組件。
import { Menu, MenuList, MenuButton, MenuItem, MenuItems, MenuPopover, MenuLink, } from "@reach/menu-button"; import "@reach/menu-button/styles.css";
這是使用Menu
組件的一種方式:
function Example() { return ( <Menu> <MenuButton>Actions</MenuButton> <MenuList> <MenuItem>Download</MenuItem> <MenuLink to="view">View</MenuLink> </MenuList> </Menu> ); }
上面的示例代碼是複合組件的實現之一,您可以在其中看到Menu
、 MenuButton
、 MenuList
、 MenuItem
和MenuLink
都是從@reach/menu-button
導入的。 與導出單個組件相反,ReachUI 導出了一個父組件,它是Menu
及其子組件,即MenuButton
、 MenuList
、 MenuItem
和MenuLink
。
什麼時候應該使用複合組件?
作為 React 開發人員,您應該在以下情況下使用複合組件:
- 解決與構建可重用組件相關的問題;
- 開發具有最小耦合的高內聚組件;
- 在組件之間共享邏輯的更好方法。
複合組件的優缺點
複合組件是一種很棒的 React 模式,可以添加到您的 React 開發人員工具包中。 在本節中,我將說明使用複合組件的優點和缺點,以及我從使用這種開發模式構建組件中學到的東西。
優點
關注點分離
將所有 UI 狀態邏輯都放在父組件中,並在內部與所有子組件進行通信,從而明確職責分工。降低複雜性
與 prop 鑽取以將屬性傳遞給它們的特定組件相反,子 props 使用複合組件模式轉到它們各自的子組件。
缺點
在 React 中使用複合組件模式構建組件的主要缺點之一是只有父組件的direct children
組件才能訪問 props,這意味著我們不能將這些組件中的任何一個包裝在另一個組件中。
export default function FlyoutMenu() { return ( <FlyOut> {/* This breaks */} <div> <FlyOut.Toggle /> <FlyOut.List> <FlyOut.Item>Edit</FlyOut.Item> <FlyOut.Item>Delete</FlyOut.Item> </FlyOut.List> </div> </FlyOut> ); }
這個問題的一個解決方案是使用靈活的複合組件模式來使用React.createContext
API 隱式共享狀態。
Context API 使得在使用 React 中構建組件的複合組件模式構建時,可以通過嵌套組件傳遞 React 狀態。 這是可能的,因為context
提供了一種將數據向下傳遞到組件樹的方法,而無需在每個級別手動向下傳遞道具。 使用 Context API 為最終用戶提供了極大的靈活性。
在 React 中維護複合組件
複合組件提供了一種更靈活的方式來在 React 應用程序中共享狀態,因此在 React 應用程序中使用複合組件可以更輕鬆地維護和實際調試應用程序。
構建演示
在本文中,我們將使用複合組件模式在 React 中構建一個手風琴組件。 我們將在本教程中構建的組件將是一個定制的手風琴組件,它靈活並通過使用 Context API 在組件內共享狀態。
我們走吧!
首先,讓我們使用以下代碼創建一個 React 應用程序:
npx create-react-app accordionComponent cd accordionComponent npm start
要么
yarn create react-app accordionComponent cd accordionComponent yarn start
上面的命令創建了一個 React 應用程序,將目錄更改為 React 項目,然後啟動開發服務器。
注意:在本教程中,我們將使用styled-components
來幫助設置組件的樣式。
使用以下命令安裝styled-components
:
yarn add styled-components
要么
npm install --save styled-components
在src文件夾中,創建一個名為components的新文件夾。 這是我們所有組件都將存在的地方。 在components文件夾中,創建兩個新文件: accordion.js
和accordion.styles.js
。
accordion.styles.js
文件包含我們對Accordion
組件的樣式(我們的樣式是使用styled-components
完成的)。
import styled from "styled-components"; export const Container = styled.div` display: flex; border-bottom: 8px solid #222; `;
上面是一個使用名為styled-components
的css-in-js
庫對組件進行樣式設置的示例。
在accordion.styles.js
文件中,添加其餘樣式:
export const Frame = styled.div` margin-bottom: 40px; `; export const Inner = styled.div` display: flex; padding: 70px 45px; flex-direction: column; max-width: 815px; margin: auto; `; export const Title = styled.h1` font-size: 40px; line-height: 1.1; margin-top: 0; margin-bottom: 8px; color: black; text-align: center; `; export const Item = styled.div` color: white; margin: auto; margin-bottom: 10px; max-width: 728px; width: 100%; &:first-of-type { margin-top: 3em; } &:last-of-type { margin-bottom: 0; } `; export const Header = styled.div` display: flex; flex-direction: space-between; cursor: pointer; margin-bottom: 1px; font-size: 26px; font-weight: normal; background: #303030; padding: 0.8em 1.2em 0.8em 1.2em; user-select: none; align-items: center; img { filter: brightness(0) invert(1); width: 24px; user-select: none; @media (max-width: 600px) { width: 16px; } } `; export const Body = styled.div` font-size: 26px; font-weight: normal; line-height: normal; background: #303030; white-space: pre-wrap; user-select: none; overflow: hidden; &.closed { max-height: 0; overflow: hidden; transition: max-height 0.25ms cubic-bezier(0.5, 0, 0.1, 1); } &.open { max-height: 0px; transition: max-height 0.25ms cubic-bezier(0.5, 0, 0.1, 1); } span { display: block; padding: 0.8em 2.2em 0.8em 1.2em; } `;
讓我們開始構建我們的手風琴組件。 在accordion.js
文件中,讓我們添加以下代碼:
import React, { useState, useContext, createContext } from "react"; import { Container, Inner, Item, Body, Frame, Title, Header } from "./accordion.styles";
上面,我們正在導入useState
、 useContext
和createContext
鉤子,這將幫助我們使用複合組件構建我們的手風琴組件。
React 文檔解釋說, context
有助於提供一種通過組件樹傳遞數據的方法,而無需在每個級別手動向下傳遞 props。
查看我們之前在accordion.js
文件中導入的內容,您會注意到我們還將樣式作為組件導入,這將幫助我們更快地構建組件。
我們將繼續為組件創建上下文,該上下文將與需要它們的組件共享數據:
const ToggleContext = createContext(); export default function Accordion({ children, ...restProps }) { return ( <Container {...restProps}> <Inner>{children}</Inner> </Container> ); }
上面代碼片段中的Container
和Inner
組件來自我們的./accordion.styles.js
文件,我們在該文件中使用styled-components
(來自css-in-js
庫)為我們的組件創建了樣式。 Container
組件容納了我們使用複合組件構建的整個Accordion
。
這裡我們使用createContext()
方法創建一個上下文對象,所以當 React 渲染一個訂閱這個 Context 對象的組件時,它會從樹中它上面最匹配的 Provider 讀取當前上下文值。
然後我們還創建了我們的基礎組件,即手風琴; 它需要children
和任何restProps
。 這是我們的父組件,其中包含 Accordion 的子組件。
讓我們在accordion.js
文件中創建其他子組件:
Accordion.Title = function AccordionTitle({ children, ...restProps }) { return <Title {...restProps}>{children}</Title>; }; Accordion.Frame = function AccordionFrame({ children, ...restProps }) { return <Frame {...restProps}>{children}</Frame>; };
注意.
在父手風琴組件之後; 這用於將子組件連接到其父組件。
讓我們繼續。 現在將以下內容添加到accordion.js
文件中:
Accordion.Item = function AccordionItem({ children, ...restProps }) { const [toggleShow, setToggleShow] = useState(true); return ( <ToggleContext.Provider value={{ toggleShow, setToggleShow }}> <Item {...restProps}>{children}</Item> </ToggleContext.Provider> ); }; Accordion.ItemHeader = function AccordionHeader({ children, ...restProps }) { const { isShown, toggleIsShown } = useContext(ToggleContext); return ( <Header onClick={() => toggleIsShown(!isShown)} {...restProps}> {children} </Header> ); }; Accordion.Body = function AccordionHeader({ children, ...restProps }) { const { isShown } = useContext(ToggleContext); return ( <Body className={isShown ? "open" : "close"}> <span>{children}</span> </Body> ); };
所以在這裡我們創建了一個Body
、 Header
和Item
組件,它們都是父組件Accordion
的子組件。 這是它可能開始變得棘手的地方。 另外,請注意這裡創建的每個子組件還接收一個children
prop 和restprops
。
在Item
子組件中,我們使用useState
鉤子初始化我們的狀態並將其設置為 true。 然後還記得我們在accordion.js
文件的頂層創建了一個ToggleContext
,它是一個Context Object
,當 React 渲染一個訂閱這個 Context 對象的組件時,它會從它上面最接近的匹配 Provider 讀取當前的上下文值在樹上。
每個 Context 對像都帶有一個Provider
React 組件,它允許消費組件訂閱上下文更改。
provider
組件接受要傳遞給作為此提供者後代的消費組件的value
屬性,這裡我們傳遞當前狀態值,即toggleShow
和設置當前狀態值setToggleShow
的方法。 它們是決定我們的上下文對像如何在不使用道具鑽取的情況下共享組件周圍狀態的值。
然後在Accordion
的header
子組件中,我們正在破壞上下文對象的值,然後在單擊時更改toggleShow
的當前狀態。 所以我們要做的是在點擊 Header 時隱藏或顯示我們的手風琴。
在我們的Accordion.Body
組件中,我們還破壞了組件的當前狀態toggleShow
,然後根據toggleShow
的值,我們可以隱藏正文或顯示Accordion.Body
組件的內容。
這就是我們的accordion.js
文件的全部內容。
現在,我們可以在這裡看到我們所學到的關於Context
和Compound components
的所有知識是如何結合在一起的。 但在此之前,讓我們創建一個名為data.json
的新文件並將以下內容粘貼到其中:
[ { "id": 1, "header": "What is Netflix?", "body": "Netflix is a streaming service that offers a wide variety of award-winning TV programs, films, anime, documentaries and more – on thousands of internet-connected devices.\n\nYou can watch as much as you want, whenever you want, without a single advert – all for one low monthly price. There's always something new to discover, and new TV programs and films are added every week!" }, { "id": 2, "header": "How much does Netflix cost?", "body": "Watch Netflix on your smartphone, tablet, smart TV, laptop or streaming device, all for one low fixed monthly fee. Plans start from £5.99 a month. No extra costs or contracts." }, { "id": 3, "header": "Where can I watch?", "body": "Watch anywhere, anytime, on an unlimited number of devices. Sign in with your Netflix account to watch instantly on the web at netflix.com from your personal computer or on any internet-connected device that offers the Netflix app, including smart TVs, smartphones, tablets, streaming media players and game consoles.\n\nYou can also download your favorite programs with the iOS, Android, or Windows 10 app. Use downloads to watch while you're on the go and without an internet connection. Take Netflix with you anywhere." }, { "id": 4, "header": "How do I cancel?", "body": "Netflix is flexible. There are no annoying contracts and no commitments. You can easily cancel your account online with two clicks. There are no cancellation fees – start or stop your account at any time." }, { "id": 5, "header": "What can I watch on Netflix?", "body": "Netflix has an extensive library of feature films, documentaries, TV programs, anime, award-winning Netflix originals, and more. Watch as much as you want, any time you want." } ]
這是我們將用於測試手風琴組件的數據。
所以讓我們繼續。 我們幾乎完成了,我相信你從這篇文章中學到了很多。
在本節中,我們將把我們一直在研究和學習的關於復合組件的所有內容匯總在一起,以便能夠在我們的App.js
文件中使用它來使用Array.map
函數來顯示我們已經在網絡上擁有的數據頁。 還要注意App.js
中沒有使用狀態; 我們所做的只是將數據傳遞給特定的組件,而 Context API 會處理所有其他事情。
現在進入最後一部分。 在您的App.js
中,執行以下操作:
import React from "react"; import Accordion from "./components/Accordion"; import faqData from "./data"; export default function App() { return ( <Accordion> <Accordion.Title>Frequently Asked Questions</Accordion.Title> <Accordion.Frame> {faqData.map((item) => ( <Accordion.Item key={item.id}> <Accordion.Header>{item.header}</Accordion.Header> <Accordion.Body>{item.body}</Accordion.Body> </Accordion.Item> ))} </Accordion.Frame> </Accordion> ); }
在您的App.js文件中,我們從文件路徑中導入了我們的 Compound Component Accordion,然後還導入了我們的虛擬數據,通過虛擬數據映射以獲得我們數據文件中的各個項目,然後按照各自的顯示組件,您還會注意到我們所要做的就是將子組件傳遞給相應的組件,Context API 負責確保它到達正確的組件並且沒有道具鑽孔。
這是我們最終產品的樣子:
複合組件的替代品
使用複合組件的替代方法是使用 Render Props API。 React 中的 Render Prop 一詞指的是一種在 React 組件之間共享代碼的技術,該技術使用值為函數的 prop。 帶有 render prop 的組件接受一個函數,該函數返回一個 React 元素並調用它,而不是實現自己的渲染邏輯。
當組件相互嵌套時,將數據從組件向下傳遞給需要數據的子組件可能會導致道具鑽孔。 這是使用 Context 在組件之間共享數據優於使用 render prop 方法的優勢。
結論
在本文中,我們了解了 React 的一種高級模式,即復合組件模式。 通過使用複合組件模式來構建組件,在 React 中構建可重用組件是一種很棒的方法,為您的組件提供了很大的靈活性。 如果您的組件目前不需要靈活性,您仍然可以選擇使用 Render Prop。
複合組件在構建設計系統中最有幫助。 我們還使用 Context API 完成了在組件內共享狀態的過程。
- 本教程的代碼可以在 Codesandbox 上找到。