实现一个功能:统一处理页面接口数据加载;并且渲染不同状态的ui。
或者是子组件;也可以自定义loading/skeleton,error。
最后:高阶组件-render props
react文档-高阶组件: https://zh-hans.reactjs.org/docs/higher-order-components.html
2023.3.8 星期三
1 React中封装组件的一些方法
# 1 SS React中封装组件的一些方法
1. extends 正向继承
对于类组件而言,可以通过extends继承某个父类,从而获得一些公共的能力1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class LogPage extends React.Component {
trackLog() {
console.log("trackLog");
}
}
class Page1 extends LogPage {
onBtnClick = () => {
console.log('click')
this.trackLog();
};
render() {
return <button onClick={this.onBtnClick}>click</button>;
}
}
借助OOP的思想,可以通过封装、继承和多态来实现数据的隔离和功能的复用。
2. HOC
2.1. 劫持props
高阶组件会返回一个新的组件,这个组件会拦截传递过来的props,这样就可以做一些特殊的处理,或者仅仅是添加一些通用的props1
2
3
4
5
function HOC(Comp) {
const commonProps = { x: 1, commonMethod1, commonMethod2 };
return props => <Comp {...commonProps} {...props} />;
}
2.2. 反向继承
高阶组件的核心思想是返回一个新的组件,如果是类组件,甚至可以通过继承的方式劫持组件原本的生命周期函数,扩展新的功能1
2
3
4
5
6
7
8
9
10
11
12function HOC(Comp){
return class SubComp extends Comp {
componentDidMount(){
// 处理新的生命周期方法,可以按需要决定是否调用supder.componentDidMount
}
render(){
// 使用原始的render
return super.render();
}
}
}
2.3. 控制渲染
比如我们需要判断某个页面是否需要登录,一种做法是直接在页面组件逻辑中编写判断,如果存在多个这种的页面,就变得重复了1
2
3
4
5
6
7export default (Comp) => (props) => {
const isLogin = checkLogin()
if (isLogin) {
return (<Comp {...props}/>)
}
return (<Redirect to={{ pathname: 'login' }}/>)
}
2.4. HOC的缺点
劫持Props是HOC最常用的功能之一,但这也是它的缺点:层级的嵌套和状态的透传。
对于HOC本身而言,传递给他的props是不需要关心的,他只是负责将props透传下去。这就要求对于一些特殊的prop如ref等,需要额外使用forwardRef才能够满足需求。
此外,我认为这也导致组件的props来源变得不清晰。最后组件经过多个HOC的装饰之后,我们就很难区分某个props注入的数据到底是哪里来的了
3. Render Props
3.1. prop传递ReactElement
React组件默认的prop: children可以实现default slot的功能
3.2. prop传递函数
但这种直接传递ReactElement也存在一些问题,那就是这些节点都是在父元素定义的。
如果能够根据组件内部的一些数据来动态渲染要展示的元素,这样就会更加灵活了。换言之,我们需要实现在组件内部动态构建渲染元素。
最简单的解决办法就是传递一个函数,由组件内部通过传参的形式通过函数动态生成需要渲染的元素1
2
3
4
5
6
7
8const Baz = ({ renderHead }) => {
const count = 123;
return <div>{renderHead(count)}</div>;
};
<Baz
renderHead={(count) => <span>count is {count}</span>}
></Baz>
通过函数的方式,可以在不改动组件内部实现的前提下,利用组件的数据实现UI分发和逻辑复用,类似于Vue的插槽作用域,也跟JavaScript中常见的回调函数作用一致。
React官方把这种技术称作Render Props:
Render Props是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
Render Props有下面几个特点
- 也是一个prop,用于父子组件之间传递数据
- 他的值是一个函数,其参数由子组件在合适的时候传入
- 通常用来render(渲染)某个元素或组件
再举一个更常用的例子,渲染列表组件,
3.3. prop传递组件
上面提到Render props是值为函数的prop,这个函数返回的是ReactElement。那不就是一个函数组件吗?既然如此,是不是也可以直接传递组件呢?答案是肯定的。
3.4. Render Props存在的问题
Render Props可以有效地以松散耦合的方式设计组件,但由于其本质是一个函数,也会存在回调嵌套过深的问题:当返回的节点也需要传入render props时,就会发生多层嵌套。
一种解决办法是使用react-adopt,它提供了组合多个render props返回结果的功能。
4. Hooks
4.1. Hooks解决的问题
React中组件分为了函数组件和Class组件,函数组件是无状态的,在Hooks之前,只能通过props控制函数组件的数据,如果希望实现一个带状态的组件,则需要通过Class组件的instace来维护。
Class组件主要有几个问题
- 逻辑分散,相互关连的逻辑分散在各个生命周期函数;每个生命周期函数又塞满了各不相同的逻辑
- 逻辑复用需要通过高阶组件HOC或者Render Props来处理,
不论是HOC还是Render Props,都需要重新组织组件结构,很容易形成组件嵌套,代码阅读性和可维护性都会变差。
因此需要一种扁平化的逻辑复用的方式,因此Hooks出现了。其优点有
- 扁平化的逻辑复用,在无需修改组件结构的情况下复用状态逻辑
- 将相互关联的部分放在一起,互不相关的地方相互隔离
- 函数式编程
## 5. 小结
本文主要总结了几种封装React组件的方式,包括正向继承、HOC、Render Props、 Hooks等方式,每种方式都有各自的优缺点。恰好最近参与了新的React项目,可以多尝试一下这些方法。
2 高阶组件(HOC)的入门及实践
# 2 React高阶组件(HOC)的入门及实践
## 使用高阶组件的原因(为什么❓)
关于高阶组件能解决的问题可以简单概括成以下三个方面:
1) 抽取重复代码,实现组件复用,常见场景:页面复用。
1) 条件渲染,控制组件的渲染逻辑(渲染劫持),常见场景:权限控制。
1) 捕获/劫持被处理组件的生命周期,常见场景:组件渲染性能追踪、日志打点。
高阶组件的实现(怎么做❓)
通常情况下,实现高阶组件的方式有以下两种:
- 属性代理(Props Proxy)
- 返回一个无状态(stateless)的函数组件
- 返回一个 class 组件
- 反向继承(Inheritance Inversion)
高阶组件实现方式的差异性决定了它们各自的应用场景:一个 React 组件包含了 props、state、ref、生命周期方法、static方法和React 元素树几个重要部分,所以我将从以下几个方面对比两
属性代理(Props Proxy)
- 属性代理是最常见的实现方式,它本质上是使用组合的方式,通过将组件包装在容器组件中实现功能。
- 属性代理方式实现的高阶组件和原组件的生命周期关系完全是React父子组件的生命周期关系,所以该方式实现的高阶组件会影响原组件某些生命周期等方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 返回一个无状态的函数组件
function HOC(WrappedComponent) {
const newProps = { type: 'HOC' };
return props => <WrappedComponent {...props} {...newProps}/>;
}
// 返回一个有状态的 class 组件
function HOC(WrappedComponent) {
return class extends React.Component {
render() {
const newProps = { type: 'HOC' };
return <WrappedComponent {...this.props} {...newProps}/>;
}
};
}
#### 操作 props
#### 抽象 state
#### 获取 refs 引用
#### 获取原组件的 static 方法
#### 通过 props 实现条件渲染
#### 用其他元素包裹传入的组件
我们可以通过类似下面的方式将原组件包裹起来,从而实现布局或者是样式的目的:
反向继承
反向继承指的是使用一个函数接受一个组件作为参数传入,并返回一个继承了该传入组件的类组件,且在返回组件的 render() 方法中返回 super.render() 方法,最简单的实现如下:1
2
3
4
5
6
7const HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
#### 劫持原组件生命周期方法
#### 读取/操作原组件的 state
#### 渲染劫持
属性代理和反向继承的对比
## 具体实践
### 页面复用
### 权限控制
### 组件渲染性能追踪
## 扩展阅读(Q & A)
### Hook 会替代高阶组件吗?
3 使用React hooks
# 3 SS 使用React hooks,些许又多了不少摸鱼时间
一、📻概述
1、关于React Hooks
• React Hooks 是一个可选功能,通常用 class 组件 来和它做比较;
• 100% 向后兼容,没有破坏性改动;
• 不会取代 class 组件,尚无计划要移除 class 组件。
2、认识React Hooks
(1)回顾React函数式组件
(2)函数组件的特点
- 没有组件实例;
- 没有生命周期;
- 没有 state 和 setState ,只能接收 props 。
(3)class组件的问题
上面我们说到了函数组件是一个纯函数,只能接收 props ,没有任何其他功能。而 class 组件拥有以上功能,但是呢,class 组件会存在以下问题: - 大型组件很难拆分和重构,很难测试(即 class 不易拆分);
- 相同业务逻辑,分散到各个方法中,逻辑混乱;
- 复用逻辑变得复杂,如 Mixins 、 HOC 、 Render Props 。
因此,有了以上问题的出现,也就有了 React Hooks 。
(4)React 组件
• React 组件更易于用函数来表达:
• React 提倡函数式编程,即 view=fn(props) ;
• 函数更灵活,更易拆分,更易测试;
• 但函数组件太简单,需要增强能力 ——因此,有了 React Hooks 。
二、🪕几种 Hooks
三、⌨️React-Hooks组件逻辑复用
1、class组件的逻辑复用
class 组件有三种逻辑复用形式。分别是:
- Mixin
- 高阶组件 HOC
- Render Prop
下面说下它们三者各自的缺点。
(1)Mixin
- 变量作用域来源不清
- 属性重名
- mixins 引入过多会导致顺序冲突
(2)高阶组件 HOC - 组件层级嵌套过多,不易渲染,不易调试
- HOC 会劫持 props ,必须严格规范,容易出现疏漏
(3)Render Prop - 学习成本高,不易理解
- 只能传递纯函数,而默认情况下纯函数功能有限
了解了三种 class 组件的缺点之后,现在,我们来看下如何使用 Hooks 做组件逻辑复用。
2、使用 Hooks 做组件逻辑复用
使用 hooks 来使得组件可以进行逻辑复用的本质是:自定义 hooks 。下面我们用一个例子来展示。
useMousePosition
4 React Hooks:发请求这件小事
# 4 React Hooks 第二期:发请求这件小事
## useData ?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function useData(dataLoader) {
const currentDataLoader = useRef(null);
// ...
useEffect(() => {
currentDataGetter.current = dataGetter;
dataLoader().then((responseData) => {
// ...
// 如果有更新的请求,放弃之前的
if (currentDataGetter.current !== dataGetter) {
return;
}
setData(responseData);
});
}, [dataLoader);
// ...
}
## 处理异步状态
以上的 useData 只能解决关于数据获取的那一部分问题,为了处理我们得到的几个状态,不免需要写出这样的代码:1
2
3
4
5
6
7
8
9
10
11
12
13if (loading && data == null) {
return <Spin />
}
if (error) {
return <Exception />
}
return (
<Spin loading={loading}>
{renderMovieList(movieListData)}
</Spin>
);
renderProps
久而久之我们会发现在所有使用这个 useData 的组件中,我们都避免不了手写这两个 if。但是 hooks 只能解决生命周期的问题,没法封装一些 render 的逻辑。其实这里最有效的解决方法就是 Suspense,但是因为还没有发布,所以我们想到了 renderProps:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26function DataBoundary({ data, loading, error, children }) {
if (loading) {
return <Spin />;
}
// ...
return <Spin loading={loading}>{children(data)}</Spin>;
}
/* */
function MovieList({ page, size }}) {
const queryMovieList = useCallback((page, size) => api.queryMovieList({ page, size }), [page, size]);
const movieListResult = useData(queryMovieList);
// const { data: movieListData, loading, error } = useData(queryMovieList);
return (
<DataBoundary {...movieListResult}>
{/* <DataBoundary data={movieListData} loading={loading} error={error}> */}
{(data) => renderMovieList(data)}
</DataBoundary>
);
function renderMovieList(movieList) {
return movieList.map(item => <MovieItem key={item.id} data={item} />)
}
}
在 DataBoundary 里,我们封装了关于异步状态处理的渲染流程,还可以提供出类似 fallback 的钩子,满足需要定制化的组件场景。用它做到了类似于 Suspense 的事情。
## All in Hooks?
但是如果我们真的想在 hooks 里完成所有的事情呢?这里再抛出一个彩蛋:
如果我们可以在 hooks 中返回一个 “renderProp”,我们就可以完整的将 render 相关的逻辑也封装在 hooks 里了,但是同时这样也会使得一个 hook 里的代码变得复杂,这里只是提供一种思路。那么这两种写法你更喜欢那个呢?
44 Suspense
# 44 React组件如何优雅地处理异步数据
PS: Suspense 文档只处理动态加载组件;并没有拦截渲染的说明。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import ErrorBoundary from "./ErrorBoundary"
import RandomWord from "./RandomWord"
import {Suspense} from 'react'
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<div>loading...</div>}>
<RandomWord />
</Suspense>
</ErrorBoundary>
)
}
export default App
5 React的render-props模式
# 5 React的render-props模式、高阶组件
props.render()模式
使用children代替render
使用displayName
使用了高阶组件之后会存在一个问题: 开发者工具会显示两个组件名称一样的组件
因为默认情况下, React会使用组件名称作为组件名称,所有在两个组件都使用了高阶组件的情况下,两个组件的外壳都是其高阶组件的名称
解决方案: 手动设置displayName
使用步骤
在高阶组件的函数内部定义一个函数getDisplayName(){}把传入组件作为参数传入这个函数
1 | import { Component } from 'react' |
111 高阶组件和axios的拦截器
# 111 React中使用高阶组件和axios的拦截器,统一处理请求失败提示1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29export default (WrappedComponent) => {
return class extends Component {
constructor(props) {
super(props)
this.state = {
error: false
}
}
componentWillMount() {
axios.interceptors.response.use((response) => {
return response;
}, (error) => {
this.setState({
error: true
})
return Promise.reject(error);
});
}
render() {
const errorElem = this.state.error ? <div>请求出错了</div> : null
return (
<div>
{ errorElem }
<WrappedComponent {...this.props}/>
</div>
)
}
}
}
# 第三方库
react-adopt
react-adopt: https://github.com/pedronauck/react-adopt
React Adopt - Compose render props components like a pro
SWR
SWR: https://swr.vercel.app/zh-CN
vercel/swr: https://github.com/vercel/swr
“SWR” 这个名字来自于 stale-while-revalidate:一种由 HTTP RFC 5861(opens in a new tab) 推广的 HTTP 缓存失效策略。这种策略首先从缓存中返回数据(过期的),同时发送 fetch 请求(重新验证),最后得到最新数据。
使用 SWR,组件将会不断地、自动获得最新数据流。
UI 也会一直保持快速响应。
useRequest
https://github.com/alibaba/hooksimport { useRequest } from 'ahooks';
1
2
3
4
5
6
7
8
9
10
11
12
13import { useRequest } from '@umijs/hooks';
function getUsername() {
return Promise.resolve('jack');
}
export default () => {
const { data, error, loading } = useRequest(getUsername)
if (error) return <div>failed to load</div>
if (loading) return <div>loading...</div>
return <div>Username: {data}</div>
}
useRequest- 蚂蚁中台标准请求 Hooks
但日常工作中,只用一个 useAsync 还是不够的,Umi Hooks 中和网络请求相关的 Hooks 就有非常多。比如和分页请求相关的 usePagination,请求自带防抖的 useSearch,内置 umi-request 的 useAPI,加载更多场景的 useLoadMore,等等等等。
同时随着 zeit/swr 的诞生,给了我们很多灵感,原来网络请求还可以这么玩!swr 有非常多好用,并且我们想不到的能力。比如:
- 屏幕聚焦重新发起请求。
- swr 能力。
能力介绍
- 基础网络请求
- 手动请求
- 轮询
- 并行请求
- 防抖 & 节流
- 缓存 & SWR & 预加载
- 屏幕聚焦重新请求
- 集成请求库
- 分页
- 加载更多
42 fetching-box
fetching-box: https://github.com/GitHubJiKe/fetching-box
React 开发中,更简洁、优雅的处理 loading、error 以及统一 Container 样式的问题的思路
React组件设计实践总结04 - 组件的思维
- 高阶组件
- Render Props
- 使用组件的方式来抽象业务逻辑
- hooks 取代高阶组件
- hooks 实现响应式编程
- 类继承也有用处
- 模态框管理
- 使用 Context 进行依赖注入
- 不可变的状态
- React-router: URL 即状态
- 组件规范
扩展
4. hooks 取代高阶组件
自定义 hook 和函数组件的代码结构基本一致, 所以有时候hooks 写着写着原来越像组件, 组件写着写着越像 hooks. 我觉得可以认为组件就是一种特殊的 hook, 只不过它输出 Virtual DOM.