react课程20-Advanced React Patterns
Last updated on October 18, 2024 am
本节课要点:1、render props patern 2、HOC 3、Compound Component Pattern
4、React Portal 5、javascript事件传播顺序 6、检测在窗体外的点击行为
“这节课看起来很高级,是因为它确实很高级🤫“
Jonas每次教新的知识都激动的很,搞得我也很激动,笑鼠。。。我倒要看看这节课讲的内容到底多么高级😋😋😋。

(什么玩意进我脑子里了??)
(怎么现在vite 和 creatreact app都不会自动配置prettier了)
一、render props patern
二、HOC
三、Compound Component Pattern
Compound Component Pattern(复合组件模式) 是一种 React 设计模式,它允许你创建一组组件,这些组件可以一起工作并共享状态。这种模式通过将状态管理和 UI 逻辑分离,使得组件的组合更加灵活和可重用。
(1)主要特征
- 状态共享:父组件管理状态,并通过上下文(
Context)提供给子组件,使它们可以访问和操作该状态。 - 易于组合:子组件可以灵活地组合在一起,形成复杂的 UI,而不需要关心状态的管理。
- 清晰的 API:使用者可以清楚地知道如何使用这些组件,组件之间的关系一目了然。
(2)使用示例
1.App.js
1 | |
2.Couter.js
居然使用到了尘封的contextAPI,已经被我忘得一干二净了hhh
根据我的理解,步骤如下:
- 创建一个context
- 创建一个父组件,用以提供状态或更多东西
- 创造子组件,子组件接收想要的状态
- 把子组件连接到父组件上
然后子组件就可以灵活地组合,还可以灵活地修改😋。
1 | |
(”恭喜你成为少数几个知道这种模式的React开发人员之一“ ,hhh,Jonas你是懂得捧杀的)
三、使用React Portal创建一个Modal Window
(1)自然创建
这样的表单是不是好看多了?
1、Modal组件
1 | |
2、AddCabin组件
这里我们把之前在cabin.jsx(page页)的状态、按钮和表单组件都拿过来了;
把onCloseModal传递给表单组件是为了点击cancel和创建完cabin的时候都能关闭表单。
1 | |
3、createCabinForm中所做的修改
在创建cabin的onSubmit的函数中的onSuccess回调函数中加入onCloseModal?.(),使得创建cabin成功后关闭表单。同时cacel按钮的onClick函数中也加入。(?.很重要)
Form组件接收type道具,因为这个表单也可能在其他地方被重用。所以如果含有onCloseModal就说明这是modal组件,type设置为modal,否则为regular。以显示不同的style:type={onCloseModal ? 'modal' : 'regular'
(2)使用React Portal
React Portal 是 React 中的一种机制,允许你将子组件渲染到父组件以外的 DOM 节点中。使用 Portal,你可以在应用的某个部分以外的地方呈现子组件,而不需要更改组件的层次结构。
1、主要特性
- DOM 节点分离:Portal 允许你在 React 组件树之外渲染子组件,这对于模态框、弹出菜单、工具提示等 UI 组件非常有用。
- 保持父组件的上下文:即使子组件被渲染到不同的 DOM 节点中,它仍然可以访问父组件的上下文。
- 不改变 React 组件层级:Portal 的使用不会影响组件的层次结构,这使得组织和维护代码变得更加清晰。
2、重要特点
通过使用 Portal 渲染的组件可以避免在其父容器或其他包含它的元素有 overflow: hidden 或 overflow: scroll 等样式时被裁剪或遮挡。
当一个组件(比如模态框、下拉菜单、工具提示等)被渲染在它的父组件内部,如果父组件有以下 CSS 属性:overflow: hidden 或 overflow: scroll,那么在父组件的边界之外的部分内容会被裁剪掉或滚动隐藏掉。这意味着如果你的模态框或菜单超出父组件的边界,它就会被遮挡,用户无法看到全部内容。
通过 React Portal,你可以将组件渲染到根节点或其他独立的 DOM 节点中,而不是当前的父组件内部。这样,它的渲染位置就不再受到父组件的 overflow 样式的限制,而是可以自由地显示在页面的任何地方。
3、如何使用
可以看到相较上面的版本,一共只做了两层修改:
return 后面添加creatPortal;返回值结尾加上document.{想要放置的位置}
1 | |
(3)使用复合组件模式来创建Modal
我咋听不懂呢:这个modal是根据isOpenModal状态变量来控制是否打开的,但是这个modal本身应该知道它当前是否打开。
1、为什么要使用复合组件模式
😶当前版本的问题(简单状态控制)
- 状态和逻辑紧耦合:
isOpenModal状态存在于AddCabin组件中,Modal 组件不知道自己是否打开,所有状态都必须通过父组件控制。这种方式对于简单场景是可以的,但如果有更多的交互需求,代码会变得复杂。 - 难以扩展:如果你需要为
Modal添加更多功能,比如额外的子组件或控制多个 modal,这种结构不够灵活。
😲为什么使用复合组件模式?
复合组件模式允许你把不同部分的逻辑和 UI 分离开,使得状态控制和 UI 展示更加解耦。可以将 Modal 组件内部的状态交给 Modal 自己管理,而不是完全依赖父组件。这样可以带来以下好处:
- 增强灵活性:通过复合组件模式,
Modal可以有更多子组件(比如标题、内容、关闭按钮等),这些子组件可以根据需要自由组合,避免将所有逻辑和样式硬编码在AddCabin中。 - 状态管理解耦:Modal 内部可以自己知道它的状态(是否打开),从而让父组件不用过度管理 Modal 的内部逻辑。你只需要管理是否调用
Modal,不必关心 Modal 内部是如何关闭或显示的。
2、使用复合组件模式来创建Modal
首先看一下AddCabin.js现在变成了什么样:
1 | |
可以看到它非常的简洁,只有子组件和传递给子组件的children组件,以及传递给子组件的必要prop。经过分析可以得出一共有两个子组件:Open和Window,Open传递要打开的window名字,并且会传递一个按钮,至于这个按钮如何实现onClick函数,需要看Mortal组件;Window传递本窗体的名字,并包裹需要显示的表单。两个window被条件渲染。
下面是Mortal.jsx的主要内容,实现复合组件的步骤与上面相同。(记得父组件要返回Provider来提供给子组件必要信息呀,Jonas美美忘记,当然我也没记住) 按钮的按动导致openName被设置为各个传递的名字,并在window函数中与接收到的窗体名字验证,如果相同就渲染,否则不渲染。
1 | |
💡接下来是要点!!!!既然AddCabin组件中没法传递关闭还是打开窗体的信息了,那按钮按动的函数如何绑定,又如何把onClseModal传递给createCabinForm组件呢?答案在下面⬇️
(4)cloneElement
在 React 中,cloneElement 是一个非常有用的工具,特别是在复合组件模式中,它允许你克隆一个 React 元素并向其添加新的属性或修改现有的属性,而不改变原始元素的外观和行为。它可以帮助你在不直接修改子组件的情况下,灵活地传递额外的 props 或绑定新的事件。
1、要点
- 克隆元素:它会基于一个现有的 React 元素(子组件)创建一个新的元素。这个克隆后的元素可以接收新的
props或覆盖现有的props。 - 保持原始结构:克隆的元素仍然保持与原始元素相同的子组件层次结构和渲染方式,不会影响现有的 JSX 结构。
- **传递额外的
props**:你可以在克隆的过程中向元素传递新的props,比如传递onClick事件处理函数或状态信息。这是增强组件的一个强大工具,特别是在复合组件模式中。
2、语法
1 | |
3、优点
- 避免直接修改子组件:你不需要在子组件的定义中明确处理某些 props,而是可以动态地通过
cloneElement传递额外的属性。这种方式可以让组件更加灵活、可复用。 - 保持组件封装性:父组件可以在不改变子组件内部实现的情况下,注入新的行为,比如添加
onClick或其他事件处理器。 - 动态扩展组件:使用
cloneElement,你可以根据上下文或者状态动态修改组件的行为,而不需要重新定义或重新渲染整个组件。
(5)检测在窗体外的点击行为
我们想要实现一个功能:点击form外的区域会关闭窗体,再把功能抽象成了一个custom hook。
1 | |
然后在Modal.js中的window函数中使用该hook,并把ref绑定到组件树上:<StyledModal ref={ref}>
1 | |
要点解析:
1、listenCapturing的必要性
转移至本节最后一个模块讲解。
2、if (ref.current && !ref.current.contains(e.target))
ref.current是通过 React 的useRefhook 获取到的 DOM 元素的引用。通常我们会使用useRef来保存一个对 DOM 元素的引用,从而能够在 React 的函数组件中访问它。ref.current指向你想检测是否被点击的元素(比如模态框、弹窗等)。在这个例子中,ref会保存模态框的引用。
如果 ref.current 存在,说明元素已经渲染到页面上,并且 ref 正常指向该 DOM 元素。
e.target是触发点击事件的元素,即用户点击的具体 DOM 元素。ref.current.contains(e.target)是用来检查这个e.target是否在ref.current所引用的元素内部。
四、使用复合组件模式创建可重用的table
(1)创建可复合使用的Table组件
重点:1、传递columns参数 2、Body接受的prop是data和render
子组件接受columns道具是为了提供给它们的条件渲染风格。
1 | |
(2)修改Table组件
1 | |
利用到了render属性。
五、使用复合组件模式创建可重用的文字菜单
真的有那么一丢丢复杂。。。不知道从何下手来记录了我都
首先让我们看看这个Menu长什么样:
(1)Menu.jsx
1、父组件Menus
两个状态变量:openId:控制打开菜单的cabin,position:控制菜单的位置,List的style的控制变量,因此需要通过计算得出,并传递给StyledList
1 | |
2、子组件Toggle.jsx(开关/切换)
rect常量是得到了Toggle的位置,然后再根据它的值来设置position
这里的逻辑运算的意思是:如果点击了这个开关,此时如果没有openId(也就是没有List被打开)或者当前的openId与此cabin的id不同(另外一个List现在正打开),就把openId设置为此cabin的id;否则的话,也就是说openId===id(当前打开的List就是这个cabin的),就把List关上(将openId设置为空)
1 | |
3、子组件List
接收position并传递给styledList组件,并且使用useHandleClickOutside hook,来使得它在检测到List外的点击后关闭。并且使用Portal,将组件呈现在body层次。
接收cabin的id,如果和openId不相同则不进行渲染。
1 | |
4、子组件Button
接收CabinRow传递的icon和onClick函数。
1 | |
5、子组件Menu
直接等于styled component,不返回实际内容。
(2)很不一样的CabinRow.jsx
因为我们的按钮打开的窗口也需要有Modal的形式,因此我们将两个复合组件模式组合在了一起。
仔细看会发现,Modal是在最外层,使得整个Menu都是其子组件,然后再用Menus.Menu包裹,首先要渲染的是Toggle(固定在屏幕上),然后是Menus.List包裹住相应的按钮,其中编辑和删除的按钮被Modal.Open包裹住,使得按钮按动后打开的窗户是Mortal形式的。
1 | |
?、事件触发阶段
在useHandleClickOutside Hook 中,listenCapturing 参数控制事件监听的方式(捕获阶段或冒泡阶段)。默认情况下,将 listenCapturing 设置为 true,表示事件是在捕获阶段被处理的。如果不传递 true,则会使用冒泡阶段监听事件。
(1)详细原因
1. 事件传播机制:
- 捕获阶段(Capturing Phase):事件从顶层元素(如
document)开始,一路向下传播到事件目标元素(你点击的元素)。 - 目标阶段(Target Phase):事件在目标元素上触发。
- 冒泡阶段(Bubbling Phase):事件从目标元素开始,向上传播回顶层元素。
2. 代码中的事件监听器:
在 useHandleClickOutside 中,当 listenCapturing 为 false(默认值)时,document.addEventListener('click', handleClick) 会在冒泡阶段添加事件监听器。
3. 事件触发的顺序:
当你点击打开按钮时:
- 目标元素的事件处理器:首先触发按钮的
onClick事件处理器,调用open函数,打开模态框。 - **事件冒泡到
document**:接下来,事件冒泡到document,触发在document上注册的冒泡阶段的事件监听器,即你的handleClick函数。
在 handleClick 中发生了什么:
- 此时,模态框已经被打开,
ref.current已经被赋值为模态框的 DOM 元素。 handleClick检查if (ref.current && !ref.current.contains(e.target))e.target是点击的打开按钮,它不在模态框内部。- 因此条件为
true,handleClick调用close函数,关闭模态框。
4. 结果:
- 模态框被打开后,立即又被关闭。
- 用户体验上表现为模态框无法被打开。
(2)为什么设置 listenCapturing 为 true 可以解决问题:
当将 listenCapturing 设置为 true,事件监听器会在捕获阶段触发。事件传播顺序变为:
- 事件捕获阶段:
- 事件从
document开始,向下传播。 - 在
document上的捕获阶段,触发handleClick。 - 此时模态框还未被打开(因为按钮的
onClick还未执行)。 ref.current尚未定义,ref.current && !ref.current.contains(e.target)为false,handleClick不执行任何操作。
- 事件从
- 目标元素的事件处理器:
- 事件到达目标元素(打开按钮)。
- 触发按钮的
onClick,调用open,模态框被打开。
- 事件冒泡阶段:
- 事件从目标元素向上冒泡,但因为你的事件监听器只在捕获阶段,不会再次触发
handleClick。
- 事件从目标元素向上冒泡,但因为你的事件监听器只在捕获阶段,不会再次触发
结果:
- 模态框成功打开。
handleClick不会在模态框打开后立即调用close。