React代码复用-组件

实现一个功能:统一处理页面接口数据加载;并且渲染不同状态的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
16
class 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,这样就可以做一些特殊的处理,或者仅仅是添加一些通用的props

1
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
12
function HOC(Comp){
return class SubComp extends Comp {
componentDidMount(){
// 处理新的生命周期方法,可以按需要决定是否调用supder.componentDidMount
}

render(){
// 使用原始的render
return super.render();
}
}
}

2.3. 控制渲染

比如我们需要判断某个页面是否需要登录,一种做法是直接在页面组件逻辑中编写判断,如果存在多个这种的页面,就变得重复了

1
2
3
4
5
6
7
export 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
8
const 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
7
const 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
21
function 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
13
if (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
26
function 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
15
import 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component } from 'react'

// 定义一个函数,在函数内部创建一个类式组件
const withMouse = WarppedComponent => {
class Mouse extends Component {
// 复用的逻辑代码
}
// 设置开发者工具显示名称,如果不设置开发者工具显示的外壳组件都是一样的
function getDisplayName(WarppedComponent) {
return WarppedComponent.displayName || WarppedComponent.name || 'Component'
}
// 给组件添加displayName属性
Mouse.displayName = `WithMouse${getDisplayName(WarppedComponent)}`
return Mouse
}

export default withMouse

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
29
export 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/hooks
import { useRequest } from 'ahooks';

1
2
3
4
5
6
7
8
9
10
11
12
13
import { 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 - 组件的思维

  1. 高阶组件
  2. Render Props
  3. 使用组件的方式来抽象业务逻辑
  4. hooks 取代高阶组件
  5. hooks 实现响应式编程
  6. 类继承也有用处
  7. 模态框管理
  8. 使用 Context 进行依赖注入
  9. 不可变的状态
  10. React-router: URL 即状态
  11. 组件规范
    扩展

4. hooks 取代高阶组件

自定义 hook 和函数组件的代码结构基本一致, 所以有时候hooks 写着写着原来越像组件, 组件写着写着越像 hooks. 我觉得可以认为组件就是一种特殊的 hook, 只不过它输出 Virtual DOM.

knowledge is no pay,reward is kindness
0%