Jacky's blog Jacky's blog
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Jacky Wang

行到水穷处,坐看云起时
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 编码专题

    • 00.React-目录
    • 10.React-组件-组件封装
      • 0.前言
      • 组件:受控与非受控
        • 问题 1:受控与非受控基础使用
        • 问题 2:如何获取非受控组件内部状态?
        • 问题 3:常见初学者犯的 BUG ?
        • 问题 4:封装组件库——如何处理内部状态?
        • 问题 5:如何让保证组件完全受控?
        • 问题 6:如何处理 defaultValue? 如何对 innerValue 维护?
        • 问题 7:使用 useMergeState 自定义钩子
      • Stale-closure 陈旧闭包示例
      • 封装一个高阶组件?
      • 封装一个具有异步边界的组件?
      • 封装一个 LazyLoad 功能组件?
      • 封装一个 loading 效果的组件?
      • 封装一个 useDataModal 自定义钩子?
    • 11.React-hooks-自定义钩子
    • 21.函数技巧 - 函数式编程之 compose 函数
    • 22.函数技巧-tapable钩子
    • 23.函数技巧-koa源码
    • 30.Promise系列-目录
    • 31.Promise-深入Promise原理
    • 32.Promise-指令式Promise改造
    • 32.promise-梳理 Promise 错误处理
    • 34.promise-竞态问题
    • 35.Promise-如何处理异步数组
  • 深入浅出 Vite

  • 快速上手 API

  • 深入浅出Babel

  • 深入浅出 React

  • 百问掘金
  • 编码专题
wangjiasheng
2023-09-10
目录

10.React-组件-组件封装

# 0.前言

本篇博客代码仓库

开一个新坑,希望以示例讲解知识点,talk is cheap,show me code

# 组件:受控与非受控

在 React 中的组件受控与非受控是一个非常基础的概念,但是却是初学者经常容易犯错的地方。

# 问题 1:受控与非受控基础使用

受控组件用法:

/* 受控组件示例用法 */
const ControlInput: React.FC = () => {
  /* 注:此时 value 不能为 undefined */
  const [value, setValue] = useState("");
  const onChange = useCallback((e) => {
    console.log("e.target.value", e.target.value);
    setValue(e.target.value);
  }, []);

  return <input value={value} onChange={onChange} />;
};
1
2
3
4
5
6
7
8
9
10
11

非受控组件用法:

/* 非受控组件使用 */
const UnControlInput: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);
  const onChange = useCallback((e) => {
    console.log("e.target.value", e.target.value);
  }, []);

  const getInstanceValue = () => {
    if (inputRef.current) {
      alert(inputRef.current.value);
    }
  };

  return (
    <div>
      <input ref={inputRef} onChange={onChange} defaultValue={"hello world"} />
      <button onClick={() => getInstanceValue()}>获取input中的值</button>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 问题 2:如何获取非受控组件内部状态?

对于非受控组件,需要通过 ref 绑定 dom 后获取:ref.current.value

/* 非受控组件使用 */
const UnControlInput: React.FC = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const getInstanceValue = () => {
    if (inputRef.current) {
      alert(inputRef.current.value);
    }
  };

  return (
    <div>
      <input ref={inputRef} />
      <button onClick={() => getInstanceValue()}>获取input中的值</button>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 问题 3:常见初学者犯的 BUG ?

受控 与 非受控 主要有以下区别:

  • 非受控模式:value = "undefined" + defaultValue = ""
  • 受控模式: value !== "undefined"

开发隐式约束为 value 和 defaultValue 无法同时输入。若同时输入,则会如下报错。

浏览器无法同时处理 value 和 defaultValue 属性。

❎ 错误示例代码:

const AllInput: React.FC<any> = (props) => {
  const [value, setValue] = useState("");

  const onChange = useCallback((e) => {
    console.log("e.target.value", e.target.value);
  }, []);

  return (
    <input value={value} onChange={onChange} defaultValue={"hello world"} />
  );
};
1
2
3
4
5
6
7
8
9
10
11

# 问题 4:封装组件库——如何处理内部状态?

组件库的封装和直接功能实践区别还是很大的,组件的类型一般分为两种:

  1. stateless 组件【无状态组件】

    此类组件在 Class Component 时期,也被称为 函数组件,不存在内部变量,组件的显示仅通过 props 传递。

    function PureFunctionComp(props) {
      return <div>{props.title}</div>;
    }
    
    1
    2
    3
  2. 组件内部存在状态维护逻辑【受控模式】

    function Input(props) {
      const [value, setValue] = useState("");
      return <input onChange={(e) => setValue(e.target.value)} value={value} />;
    }
    
    1
    2
    3
    4

实际开发过程中,对于第二种封装形式往往会新增一个需求,Input 组件存在两个控制来源:

  • value ← props.value 发生变化

    当外部传入 value ,props.value 改变时,input 发生变化【父组件受控】

  • value ← onChange 变化

    组件内存在 onChange 发生变化时,input 也能改变【子组件内部受控】

此时,传统的做法是将第二种组件封装方式改写为第一种组件封装方式,即 【受控模式】→ 【stateless 组件】,示例如下:

function App(){
  const [value, setValue] = useState("");
  /* 将内部 val 状态通过回调 callback 获取 */
  const onChange = (val) => setValue(val);
  return <Input value={value} onChange={onChange}>;
};

function Input(props){
  return <input onChange={props.onChange} value={props.value} />
}
1
2
3
4
5
6
7
8
9
10

综上,当封装组件库给项目组使用时,上面这套改造方案就不合适了,需要做到以下两点:

  • 当没有传递 value 属性时,子组件内部维护一套状态,以及状态的修改机制。【非受控】

    典型的如 antd 的 form 组件,通过 formRef.current.getFieldsValue() 来实时获取字段值。

  • 当传递 value 属性时,子组件又可切换为受控模式,完全取决于父组件的属性传递。

解决方案

  1. 使用 innerValue 让 <input /> 组件完全受控,难点在状态的初始化。
  2. 处理 innerValue 受控条件。

完整代码:

/* 同时兼容: defaultValue + value + onChange 属性 */
const Input = React.memo((props: any) => {
  const {
    /* controlled attribute*/
    value,
    /* uncontrolled attributes */
    defaultValue,
    onChange,
    ...rest
  } = props;

  /**
   * 初始化:维护内部 innerValue 状态,input 采用完全受控模式
   */
  const [innerValue, setInnerValue] = useState(() => {
    if (typeof value !== "undefined") {
      return value;
    } else {
      // 当 value 为 undefined 时,返回 defaultValue 值
      return defaultValue;
    }
  });

  const _onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const inputValue = e.target.value;
    /* 非受控状态:取决内部组件 onChange 回调得到的值 */
    if (typeof value === "undefined") {
      setInnerValue(inputValue);
    }
    onChange && onChange(e);
  };

  /* 受控状态:取决于外部 props.value 值 */
  useEffectUpdate(() => {
    setInnerValue(value);
  }, [value]);

  /* 保证最后的状态始终受控 */
  function fixControlledValue<T>(value: T): string {
    if (typeof value === "undefined" || value === null) {
      return "";
    }
    return String(value);
  }

  /* 改造思路:完全采用受控模式 */
  return (
    <input
      value={fixControlledValue(innerValue)}
      onChange={_onChange}
      {...rest}
    />
  );
});
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 问题 5:如何让保证组件完全受控?

需保证:value !== undefined

const [innerValue, setInnerValue] = useState(()=>{
  .....省略部分逻辑代码
})

/* 保证最后的状态始终受控 */
function fixControlledValue<T>(value: T): string {
  if (typeof value === "undefined" || value === null) {
    return "";
  }
  return String(value);
}

<input value={fixControlledValue(innerValue)} />
1
2
3
4
5
6
7
8
9
10
11
12
13

# 问题 6:如何处理 defaultValue? 如何对 innerValue 维护?

  1. 让 input 组件完全受控

    使用 innerValue 维护,核心在于初始化。

    const [innerValue, setInnerValue] = useState(() => {
      if (typeof value !== "undefined") {
        return value;
      } else {
        // 当 value 为 undefined 时,返回 defaultValue 值
        return defaultValue;
      }
    });
    
    1
    2
    3
    4
    5
    6
    7
    8

    绑定变量时保证状态始终受控,即 value 值不允许为 undefined

    /* 保证最后的状态始终受控 */
    function fixControlledValue<T>(value: T): string {
      if (typeof value === "undefined" || value === null) {
        return "";
      }
      return String(value);
    }
    
    1
    2
    3
    4
    5
    6
    7
  2. 如何更新 innerValue

    • 对于 受控模式下 ,需监听 props.value 的变化。

      useEffectUpdate(() => {
        setInnerValue(props.value);
      }, [props.value]);
      
      1
      2
      3
    • 对于 非受控模式下 ,通过 onChange 属性改变

      const onChange = (e) => {
        const inputValue = e.target.value;
        if (typeof value === "undefined") {
          setInnerValue(inputValue);
        }
      };
      
      1
      2
      3
      4
      5
      6

# 问题 7:使用 useMergeState 自定义钩子

在 rc-util 工具函数中,提供了 useMergeState 自定义钩子,已对上述逻辑进行了抽离和简化。

useMergeState 的使用方式也很简单

原先的直接透传给 input :<input value={value} defaultValue={defaultValue}>

先透传给 useMergeState ,再绑定在 input 上

const [innerValue, setInnerValue] = useMergedState("defaultValue", {
    value,
    onChange, /* innerValue 变化时触发 */
    defaultValue,
});

<input value={innerValue}>
1
2
3
4
5
6
7

其中:useMergedState 的第一个参数为 defaultValue,初始化时,优先级如下:

props.value => props.defaultValue => 自身的 defaultValue(支持传递 null)
1

完整代码如下:

import { useMergedState } from "rc-util";
/* 同时兼容: defaultValue + value + onChange 属性 */
const Input = React.memo((props: any) => {
  const {
    /* controlled attribute*/
    value,
    /* uncontrolled attribute */
    defaultValue,
    onChange,
    ...rest
  } = props;

  /* 使用自定义钩子 useMergeState */
  const [innerValue, setInnerValue] = useMergedState(null, {
    value,
    onChange,
    defaultValue,
  });

  /* 更新内部变量 */
  const _onChange = (e) => {
    setInnerValue(e.target.value);
  };

  return <input value={innerValue} onChange={_onChange} />;
});

export default () => {
  const [value, setValue] = useState("");
  return (
    <Input
      value={value}
      onChange={(e) => {
        console.log("非受控内部状态", e);
      }}
      defaultValue={"hello world"}
    />
  );
};
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
30
31
32
33
34
35
36
37
38
39

# Stale-closure 陈旧闭包示例

js 闭包示例:

❎ 错误案例 1:setTimeout

/* 陈旧闭包 */
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  });
}

/* 解决方案1: var → let */
for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  });
}

/* 解决方案2: 立即执行函数包裹 */
for (var i = 0; i < 5; i++) {
  (() => {
    setTimeout(() => {
      console.log(i);
    });
  })(i);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

示例案例 2:

function outerTest() {
  var num = 0; // 闭包变量(内存变量)
  function innetTest() {
    ++num;
    console.log(num); // num 在函数定义时就已经决定了
  }
  return innetTest;
}

const fn1 = outerTest(); // 闭包1
const fn2 = outerTest(); // 闭包2

fn1(); // 1
fn1(); // 2
fn1(); // 3
fn2(); // 1
fn2(); // 2
fn2(); // 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

特性:

  • 一个闭包内对变量的修改,不会影响到另外一个闭包中的变量
  • 在内存中保持变量数据一直不丢失

具体见另一篇博客:《函数闭包与 this 指针》

React 闭包示例:

❎ 错误案例1 :在 setTimeout 或者 setInterval 中引用外部 state

import React, { useState, useEffect } from "react";

function Counter() {
  // 闭包变量
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      console.log(count);
      setCount(count + 1); // count 永远为 0
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return <div>{count}</div>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

❎ 错误案例2 :react 点击按钮自增三次

import { useState } from "react";

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 1); // number 永远为 0
          setNumber(number + 1);
          setNumber(number + 1);
        }}
      >
        +3
      </button>
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

典型官网示例:https://zh-hans.react.dev/learn/state-as-a-snapshot

官方解释:state as a snapshot jsx 生成的时候,每次相当于生成一张快照。

# 封装一个高阶组件?

# 封装一个具有异步边界的组件?

# 封装一个 LazyLoad 功能组件?

# 封装一个 loading 效果的组件?

# 封装一个 useDataModal 自定义钩子?

编辑 (opens new window)
上次更新: 2023/10/02, 17:10:00
00.React-目录
11.React-hooks-自定义钩子

← 00.React-目录 11.React-hooks-自定义钩子→

最近更新
01
如何理解浏览器的 user agent 用户代理的含义?
11-05
02
浏览器事件循环机制
10-31
03
浏览器页面渲染机制【2023】
10-15
更多文章>
Theme by Vdoing | Copyright © 2020-2023
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式