vlambda博客
学习文章列表

React 中的复合组件

复合组件是 React 的高级模式之一,它利用一种有趣的方式来传达 UI 组件之间的关系,并通过利用显式的父子关系来共享隐式状态。


复合组件可帮助开发人员构建更具表现力和灵活性的 API,以在组件内共享状态和逻辑。本教程解释了如何借助 Context API 和 React 来使用这种高级模式构建组件来实现这一点。


注意:为了能够继续学习,您需要对 React 以及 Context API 的工作原理有基本的了解。


什么是复合成分? 


复合组件可以说是一种模式,它包含了一组组件的状态和行为,但仍将其可变部分的渲染控制权交还给外部用户。


从上面的定义中,注意关键字:state和behavior。这有助于我们理解复合组件处理状态(即状态如何在由作为组件父级的外部用户包围的组件中表现)。


复合组件的目标是为父子组件之间的通信提供更具表现力和灵活性的 API。


把它想象成HTML中的<select>and标签:<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 中构建组件的复合组件模式。此示例是UI 包中的一个Menu组件。@reach

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> );}


上面的示例代码是复合组件的实现之一,您可以在其中看到MenuMenuButtonMenuList和都是从. 与导出单个组件相反,ReachUI 导出一个父组件,该组件伴随其子组件,即、和.MenuItemMenuLink@reach/menu-buttonMenuMenuButtonMenuListMenuItemMenuLink


什么时候应该使用复合组件? 


作为 React 开发人员,您应该在以下情况下使用复合组件:


  • 解决与构建可重用组件相关的问题;


  • 开发具有最小耦合的高内聚组件;


  • 在组件之间共享逻辑的更好方法。


复合组件的优缺点 


复合组件是一种很棒的 React 模式,可以添加到您的 React 开发人员工具包中。在本节中,我将说明使用复合组件的优点和缺点,以及我从使用这种开发模式构建组件中学到的东西。


优点 

  • 关注点分离 将


    所有 UI 状态逻辑都放在父组件中,并在内部与所有子组件进行通信,从而明确划分责任。


  • 降低复杂性


    与将属性向下传递给特定组件的道具钻取相反,子道具使用复合组件模式转到它们各自的子组件。


缺点 


在 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.createContextAPI 隐式共享状态。


Context API 使得在使用 React 中构建组件的复合组件模式构建时,可以通过嵌套组件传递 React 状态。这是可能的,因为context提供了一种将数据向下传递到组件树的方法,而无需在每个级别手动向下传递 props。使用 Context API 为最终用户提供了极大的灵活性。


在 REACT 中维护复合组件 


复合组件提供了一种更灵活的方式来在 React 应用程序中共享状态,因此在 React 应用程序中使用复合组件可以更轻松地维护和实际调试应用程序。


构建演示 


在本文中,我们将使用复合组件模式在 React 中构建一个手风琴组件。我们将在本教程中构建的组件将是一个定制的手风琴组件,它灵活并通过使用 Context API 在组件内共享状态。

我们走吧!


首先,让我们使用以下代码创建一个 React 应用程序:


npx create-react-app accordionComponentcd accordionComponentnpm start


或者


yarn create react-app accordionComponentcd accordionComponentyarn 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;`;


上面是一个使用css-in-js名为styled-components.


在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-componentscss-in-jsContainerAccordion


这里我们使用该createContext()方法创建一个上下文对象,所以当 React 渲染一个订阅这个 Context 对象的组件时,它会从树中它上面最接近的匹配 Provider 读取当前上下文值。


然后我们还创建了我们的基础组件,即手风琴;它需要childrenand any 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。这是它可能开始变得棘手的地方。另外,请注意这里创建的每个子组件也会收到一个childrenprop 和restprops.


从子组件中,我们使用钩子Item初始化我们的状态并将其设置为 true。useState然后还记得我们在文件ToggleContext的顶层创建了 a ,accordion.js它是 a Context Object,当 React 渲染一个订阅这个 Context 对象的组件时,它会从树中它上面最匹配的 Provider 读取当前的上下文值。


每个 Context 对象都带有一个ProviderReact 组件,允许使用组件订阅上下文更改。


该provider组件接受value要传递给作为此提供者后代的消费组件的道具,这里我们传递当前状态值,这是toggleShow设置当前状态值的 and 方法setToggleShow。它们是决定我们的上下文对象如何在不使用道具钻取的情况下共享组件周围状态的值。


然后在我们的header子组件中Accordion,我们正在破坏上下文对象的值,然后更改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 完成了在组件内共享状态的过程。


如果看了觉得有帮助的,请关注兴码通,兴子会持续分享更多知识点,前端后端都会有哦!




- END-


点击关注兴码通,回复“1024”获取2TB学习资源!


点击关注兴码通,回复“前端干货合集”获取更多前端干货知识!

更多推荐