详解4种不同的React组件设计模式

3,082 阅读5分钟

本篇文章将介绍4种不同的组件设计模式,其中包含了每个模式的优缺点以及在代码中的使用方式;

image.png

问题

作为React的使用人员,本人在做组件库开发时,一般会思考以下问题

  • 怎么才能让组件适配更多的使用场景?
  • 怎么才能设计出比较简单并且合理的API,让它变得易用?
  • 怎么才能让该组件的UI界面和功能有较强的可扩展性?
  • ...

对这些问题的反复🤔思考,并在React相关社区反复求证,总结出以下四种组件开发模式;

为了方便讲解,接下来将以 Counter 组件的设计为例,对这四种模式进行讲解:

image.png

代码仓库 github.com/wugaoliang1…

我们首先定义两个标准:

  1. Inversion of control:组件使用的灵活性,以及对组件可控制的级别
  2. Implementation complexity: 使用组件以及实现改组件功能的复杂度

另外,在文章中讲解到每个组件设计模式的时候,我会推荐一些在一些开源组件库中又代表的示例

1. 复合组件模式

如果想要设计一个定制化程度高,API方便理解的组件,可以考虑这个模式;这种模式不会出现多层props传递的情况;

代码示例: github.com/alex83130/a…

import React from "react";
import { Counter } from "./Counter";

function Usage() {
  const handleChangeCounter = (count) => {
    console.log("count", count);
  };

  return (
    <Counter onChange={handleChangeCounter}>
      <Counter.Decrement icon="minus" />
      <Counter.Label>Counter</Counter.Label>
      <Counter.Count max={10} />
      <Counter.Increment icon="plus" />
    </Counter>
  );
}

export { Usage };

优点:

降低组件 API 的复杂度:

不再把所有的 props 都放在一个大的父容器组件中, 再传递给子组件; 而是直接将相应的props附加在相应的子组件上。这样做API的复杂度是最低的。

image.png

灵活的UI结构:

这种模式下;开发出的组件有较强的的 UI 灵活性;允许从单个组件创建各种情况。例如,用户可以更改子组件的顺序或者定义应该显示哪一个

image.png

职责分离:

更多的逻辑以及组件的 stateCounter 组件中,通过 React.Context 共享 state 以及方法给所有的子节点,做到组件间的通讯;每个组件都有比较明确的分工

image.png

缺点:

界面灵活性太大:

拥有灵活性的同时,还有可能引发意外行为; 例如

  • 将不需要的组件的子组件放置在一起
  • 将组件的子组件弄乱
  • 忘记包含强制子组件 使用不当,就会出现各种奇怪问题,而且会导致做出来的系统没有按照标准的公司交互规范去做;

image.png

写更多的 JSX:

这样设计出的组件将会增加JSX的行数;如果过使用的 eslint、Prettier 等code formatter工具可能行数更多;

从单个组件来看没有大问题,整个页面随着使用组件的变多,就会出现较大的影响

image.png

准则

  • Inversion of control: 1/4
  • Implementation complexity: 1/4

使用该模式的组件库

2. Props 受控

这个模式理解比较容易,组件转换为受控组件,通过直接修改 Props 影响组件内部的状态;一般在输入性组件中比较常用这种模式

Github: github.com/alex83130/a…

import React, { useState } from "react";
import { Counter } from "./Counter";

function Usage() {
  const [count, setCount] = useState(0);

  const handleChangeCounter = (newCount) => {
    setCount(newCount);
  };
  return (
    <Counter value={count} onChange={handleChangeCounter}>
      <Counter.Decrement icon={"minus"} />
      <Counter.Label>Counter</Counter.Label>
      <Counter.Count max={10} />
      <Counter.Increment icon={"plus"} />
    </Counter>
  );
}

export { Usage };

优点:

提供更多的控制:

由于组件的一些状态暴露在组件之外,允许用户通过控制它,而直接影响组件

image.png

缺点:

实现复杂度高:

需要有更多的逻辑对受控值进行监控,并要做判断出入的值类型是否正确等方面;实现起来复杂;

准则

  • Inversion of control: 2/4
  • Implementation complexity: 1/4

使用该模式的组件库

3. 自定义钩子

将主要的逻辑转移到一个自定义钩子中。用户可以访问这个钩子,并公开了几个内部逻辑(状态、处理程序) ,使用户能够更好地控制组件。

Github: github.com/alex83130/a…

import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

function Usage() {
  const { count, handleIncrement, handleDecrement } = useCounter(0);
  const MAX_COUNT = 10;

  const handleClickIncrement = () => {
    //Put your custom logic
    if (count < MAX_COUNT) {
      handleIncrement();
    }
  };

  return (
    <>
      <Counter value={count}>
        <Counter.Decrement
          icon={"minus"}
          onClick={handleDecrement}
          disabled={count === 0}
        />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment
          icon={"plus"}
          onClick={handleClickIncrement}
          disabled={count === MAX_COUNT}
        />
      </Counter>
      <button onClick={handleClickIncrement} disabled={count === MAX_COUNT}>
        Custom increment btn 1
      </button>
    </>
  );
}

export { Usage };

优点:

提供更多的控制:

用户可以在钩子和 JSX 元素之间插入自己的逻辑,允许修改组件的默认行为

image.png

缺点:

实现复杂度:

由于逻辑部分呈现部分分离,因此用户必须将两者链接起来。要正确实现组件,需要对组件的工作方式有很好的理解

image.png

准则

  • Inversion of control: 2/4
  • Implementation complexity: 2/4

使用该模式的组件库

4. Props Getters

关于[模式3]中的自定义 Hooks 提供了很好的控制方式;但是比较难以集成,使用者需要按照组件提供的 HooksReact native Hooks、定义state 相结合进行编写逻辑;提高了集成的复杂度;

Props Getters 模式试图去掩盖这种模式的复杂度;通过 自定义 hooks 或者 ref,返回一系列的函数或者属性;每个函数和属性都有比较明确的命名,方便使用者将 JSX 正确的关联起来;

Github: github.com/alex83130/a…

import React from "react";
import { Counter } from "./Counter";
import { useCounter } from "./useCounter";

const MAX_COUNT = 10;

function Usage() {
  const {
    count,
    getCounterProps,
    getIncrementProps,
    getDecrementProps
  } = useCounter({
    initial: 0,
    max: MAX_COUNT
  });

  const handleBtn1Clicked = () => {
    console.log("btn 1 clicked");
  };

  return (
    <>
      <Counter {...getCounterProps()}>
        <Counter.Decrement icon={"minus"} {...getDecrementProps()} />
        <Counter.Label>Counter</Counter.Label>
        <Counter.Count />
        <Counter.Increment icon={"plus"} {...getIncrementProps()} />
      </Counter>
      <button {...getIncrementProps({ onClick: handleBtn1Clicked })}>
        Custom increment btn 1
      </button>
      <button {...getIncrementProps({ disabled: count > MAX_COUNT - 2 })}>
        Custom increment btn 2
      </button>
    </>
  );
}

export { Usage };

优点:

易用性:

方便将组件集成到已有的页面中;使用者只需要将相应的 getter 放置在合适的 JSX 中,而不需要再定义一些 state 赋值给组件

image.png

缺点:

缺乏可见性:

犹豫整过组件的使用以及赋值等操作,均有 getter 进行管理;导致整个使用流程是不透明的。用户想要修改部分逻辑,就需要要详细的知道每个 getter 相应的逻辑

准则

  • Inversion of control: 3/4
  • Integration complexity: 3/4

使用该模式的组件库

总结

以上的4种模式,应该已经覆盖了大部分组件的设计方式;

总体来说,设计的组件越灵活,功能也就越强大;但是,用户的学习成本就越高,远离了即插即用的思维模式。作为开发人员,建议大家根据自己的业务逻辑以及使用人群,灵活使用以上的设计模式;同时也可以结合使用。

参考文章

5 Advanced React Patterns

kentcdodds