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 的useRef
hook 获取到的 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
。