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 上找到。