React组件设计实践总结04 - 组件的思惟

在 React 的世界里”一切都是组件“, 组件能够映射做函数式编程中的函数,React 的组件和函数同样的灵活的特性不只仅能够用于绘制 UI,还能够用于封装业务状态和逻辑,或者非展现相关的反作用, 再经过组合方式组成复杂的应用. 本文尝试解释用 React 组件的思惟来处理常见的业务开发场景.javascript

系列目录html


目录前端




1. 高阶组件

在很长一段时期里,高阶组件都是加强和组合 React 组件的最流行的方式. 这个概念源自于函数式编程的高阶函数. 高阶组件能够定义为: 高阶组件是函数,它接收原始组件并返回原始组件的加强/填充版本:vue

const HOC = Component => EnhancedComponent;
复制代码

首先要明白咱们为何须要高阶组件:java

React 的文档说的很是清楚, 高阶组件是一种用于复用组件逻辑模式. 最为常见的例子就是 redux 的connect和 react-router 的 withRouter. 高阶组件最初用于取代 mixin(了解React Mixin 的前世此生). 总结来讲就是两点:react

  • 逻辑复用. 把一些通用的代码逻辑提取出来放到高阶组件中, 让更多组件能够共享
  • 分离关注点. 在以前的章节中提到"逻辑和视图分离"的原则. 高阶组件能够做为实现该原则的载体. 咱们通常将行为层或者业务层抽取到高阶组件中来实现, 让展现组件只关注于 UI

高阶组件的一些实现方法主要有两种:git

  • 属性代理(Props Proxy): 代理传递给被包装组件的 props, 对 props 进行操做. 这种方式用得最多. 使用这种方式能够作到:github

    • 操做 props
    • 访问被包装组件实例
    • 提取 state
    • 用其余元素包裹被包装组件
  • 反向继承(Inheritance Inversion): 高阶组件继承被包装的组件. 例如:spring

    function myhoc(WrappedComponent) {
      return class Enhancer extends WrappedComponent {
        render() {
          return super.render();
        }
      };
    }
    复制代码

    能够实现:编程

    • 渲染劫持: 即控制被包装组件的渲染输出.
    • 操做 state: state 通常属于组件的内部细节, 经过继承的方式能够暴露给子类. 能够增删查改被包装组件的 state, 除非你知道你在干什么, 通常不建议这么作.

实际上高阶组件能作的不止上面列举的, 高阶组件很是灵活, 全凭你的想象力. 读者能够了解 recompose这个库, 简直把高阶组件玩出花了.

总结一下高阶组件的应用场景:

  • 操做 props: 增删查改 props. 例如转换 props, 扩展 props, 固定 props, 重命名 props
  • 依赖注入. 注入 context 或外部状态和逻辑, 例如 redux 的 connnect, react-router 的 withRouter. 旧 context 是实验性 API, 因此不少库都不会将 context 保留出来, 而是经过高阶组件形式进行注入
  • 扩展 state: 例如给函数式组件注入状态
  • 避免重复渲染: 例如 React.memo
  • 分离逻辑, 让组件保持 dumb

高阶组件相关文档在网上有不少, 本文不打算展开描述. 深刻了解高阶组件

高阶组件的一些规范:

  • 包装显示名字以便于调试

    function withSubscription(WrappedComponent) {
      class WithSubscription extends React.Component {
        /* ... */
      }
      WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
      return WithSubscription;
    }
    
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    复制代码
  • 使用 React.forwardRef 来转发 ref

  • 使用'高阶函数'来配置'高阶组件', 这样可让高阶组件的组合性最大化. Redux 的 connect 就是典型的例子

    const ConnectedComment = connect(
      commentSelector,
      commentActions,
    )(Comment);
    复制代码

    当使用 compose 进行组合时就能体会到它的好处:

    // 🙅 不推荐
    const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
    
    // ✅ 使用compose方法进行组合
    // compose(f, g, h) 和 (...args) => f(g(h(...args)))是同样的
    const enhance = compose(
      // 这些都是单独一个参数的高阶组件
      withRouter,
      connect(commentSelector),
    );
    
    const EnhancedComponent = enhance(WrappedComponent);
    复制代码
  • 转发全部不相关 props 属性给被包装的组件

    render() {
      const { extraProp, ...passThroughProps } = this.props;
      // ...
      return (
        <WrappedComponent
          injectedProp={injectedProp}
          {...passThroughProps}
        />
      );
    }
    复制代码
  • 命名: 通常以 with*命名, 若是携带参数, 则以 create*命名




2. Render Props

Render Props(Function as Child) 也是一种常见的 react 模式, 好比官方的 Context APIreact-spring 动画库. 目的高阶组件差很少: 都是为了分离关注点, 对组件的逻辑进行复用; 在使用和实现上比高阶组件要简单, 在某些场景能够取代高阶组件. 官方的定义是:

是指一种在 React 组件之间使用一个值为函数的 prop 在 React 组件间共享代码的简单技术

React 并无限定任何 props 的类型, 因此 props 也能够是函数形式. 当 props 为函数时, 父组件能够经过函数参数给子组件传递一些数据进行动态渲染. 典型代码为:

<FunctionAsChild>{() => <div>Hello,World!</div>}</FunctionAsChild>
复制代码

使用示例:

<Spring from={{ opacity: 0 }} to={{ opacity: 1 }}>
  {props => <div style={props}>hello</div>}
</Spring>
复制代码

某种程度上, 这种模式相比高阶组件要简单不少, 无论是实现仍是使用层次. 缺点也很明显:

  • 可读性差, 尤为是多层嵌套状况下
  • 组合性差. 只能经过 JSX 一层一层嵌套, 通常不宜多于一层
  • 适用于动态渲染. 由于局限在 JSX 节点中, 当前组件是很难获取到 render props 传递的数据. 若是要传递给当前组件仍是得经过 props, 也就是经过高阶组件传递进来

再开一下脑洞. 经过一个 Fetch 组件来进行接口请求:

<Fetch method="user.getById" id={userId}>
  {({ data, error, retry, loading }) => (
    <Container>
      {loading ? (
        <Loader />
      ) : error ? (
        <ErrorMessage error={error} retry={retry} />
      ) : data ? (
        <Detail data={data} />
      ) : null}
    </Container>
  )}
</Fetch>
复制代码

在 React Hooks 出现以前, 为了给函数组件(或者说 dumb component)添加状态, 一般会使用这种模式. 好比 react-powerplug

官方文档




3. 使用组件的方式来抽象业务逻辑

大部分状况下, 组件表示是一个 UI 对象. 其实组件不仅仅能够表示 UI, 也能够用来抽象业务对象, 有时候抽象为组件能够巧妙地解决一些问题.

举一个例子: 当一个审批人在审批一个请求时, 请求发起者是不能从新编辑的; 反之发起者在编辑时, 审批人不能进行审批. 这是一个锁定机制, 后端通常使用相似心跳机制来维护这个'锁', 这个锁能够显式释放,也能够在超过必定时间没有激活时自动释放,好比页面关闭. 因此前端一般会使用轮询机制来激活锁.

通常的实现:

class MyPage extends React.Component {
  public componentDidMount() {
    // 根据一些条件触发, 可能还要监听这些条件的变化,而后中止加锁轮询. 这个逻辑实现起来比较啰嗦
    if (someCondition) {
      this.timer = setInterval(async () => {
        // 轮询
        tryLock();
        // 错误处理,能够加锁失败...
      }, 5000);
    }
  }

  public componentWillUnmount() {
    clearInterval(this.timer);
    // 页面卸载时显式释放
    releaseLock();
  }

  public componentDidUpdate() {
    // 监听条件变化,开始或中止锁定
    // ...
  }
}
复制代码

随着功能的迭代, MyPage 会变得愈来愈臃肿, 这时候你开始考虑将这些业务逻辑抽取出去. 通常状况下经过高阶组件或者 hook 来实现, 但都不够灵活, 好比条件锁定这个功能实现起来就比较别扭.

有时候考虑将业务抽象成为组件, 可能能够巧妙地解决咱们的问题, 例如 Locker:

/**
 * 锁定器
 */
const Locker: FC<{ onError: err => boolean, id: string }> = props => {
  const {id, onError} = props
  useEffect(() => {
    let timer
    const poll = () => {
      timer = setTimeout(async () => {
        // ...
        // 轮询,处理异常等状况
      }, 5000)
    }

    poll()

    return () => {
      clearTimeout(timer)
      releaseLock()
    }
  }, [id])

  return null
};
复制代码

使用 Locker

render() {
  return (<div>
    {someCondition && <Locker id={this.id} onError={this.handleError}></Locker>}
  </div>)
}
复制代码

这里面有一个要点:咱们将一个业务抽象为了一个组件后,业务逻辑有了和组件同样的生命周期。如今组件内部只需关心自身的逻辑,好比只关心资源请求和释放(即 How),而什么时候进行,什么条件进行(即 When)则由父级来决定, 这样就符合了单一职责原则。 上面的例子父级经过 JSX 的条件渲染就能够动态控制锁定, 比以前的实现简单了不少




4. hooks 取代高阶组件

我的以为 hooks 对于 React 开发来讲是一个革命性的特性, 它改变了开发的思惟和模式. 首先要问一下, "它解决了什么问题? 带来了什么新的东西?"

hooks 首先是要解决高阶组件或者 Render Props 的痛点的. 官方在'动机'上就说了:

    1. 很难在组件之间复用状态逻辑:
    • 问题: React 框架自己并无提供一种将可复用的逻辑注入到组件上的方式/原语. RenderProps 和高阶组件只是'模式层面(或者说语言层面)'的东西:

    • 此前的方案: 高阶组件和 Render Props。这些方案都是基于组件自己的机制

      • 高阶组件和 Render Props 会形成多余的节点嵌套. 即 Wrapper hell
      • 须要调整你的组件结构, 会让代码变得笨重, 且难以理解
      • 高阶组件复杂, 难以理解
      • 此前高阶组件也要 ref 转发问题等等
    • hooks 如何解决:

      • 将状态逻辑从组件中脱离, 让他能够被单独的测试和复用.
      • hooks 能够在组件之间共享, 不会影响组件的结构

    1. 复杂的组件难以理解: 复杂组件的特色是有一大堆分散的状态逻辑和反作用. 例如每一个生命周期函数经常包含一些互不相关的逻辑, 这些互不相关的逻辑会慢慢变成面条式的代码, 可是你发现很难再对它们进行拆解, 更别说测试它们
    • 问题:

      • 实际状况,咱们很难将这些组件分解成更小的组件,由于状态处处都是。测试它们也很困难。
      • 常常致使过度抽象, 好比 redux, 须要在多个文件中跳转, 须要不少模板文件和模板代码
    • 此前的解决方法: 高阶组件和 Render Props 或者状态管理器. 分割抽离逻辑和 UI, 切割成更小粒度的组件

    • hooks 如何解决: Hooks 容许您根据相关部分(例如设置订阅或获取数据)将一个组件分割成更小的函数,而不是强制基于生命周期方法进行分割。你还能够选择使用一个 reducer 来管理组件的本地状态,以使其更加可预测


    1. 基于 class 的组件对机器和用户都不友好:
    • 问题:
      • 对于人: 须要理解 this, 代码冗长
      • 对于机器: 很差优化
    • hooks 如何解决: 函数式组件
    • 新的问题: 你要了解闭包

Hooks 带来的新东西: hook 旨在让组件的内部逻辑组织成可复用的更小单元,这些单元各自维护一部分组件‘状态和逻辑’

migrate to hooks

图片来源于twitter(@sunil Pai)

  • 一种新的组件编写方式. 和此前基于 class 或纯函数组件的开发方式不太同样, hook 提供了更简洁的 API 和代码复用机制, 这使得组件代码变得更简短. 例如 👆 上图就是迁移到 hooks 的代码结构对比, 读者也能够看这个演讲(90% Cleaner React).

  • 更细粒度的状态控制(useState). 之前一个组件只有一个 setState 集中式管理组件状态, 如今 hooks 像组件同样, 是一个逻辑和状态的聚合单元. 这意味着不一样的 hook 能够维护本身的状态.

  • 无论是 hook 仍是组件,都是普通函数.

    • 从某种程度上看组件和 hooks 是同质的(都包含状态和逻辑). 统一使用函数形式开发, 这使得你不须要在类、高阶组件或者 renderProps 上下文之间切换, 下降项目的复杂度. 对于 React 的新手来讲,各类高阶组件、render props 各类概念拉高了学习曲线
    • 函数是一种最简单的代码复用单元, 最简单也意味着更灵活。相比组件的 props,函数的传参更加灵活; 函数也更容易进行组合, hooks 组合其余 hook 或普通函数来实现复杂逻辑.
    • 本质上讲,hooks 就是给函数带来了状态的概念
  • 高阶组件之间只能简单嵌套复合(compose), 而多个 hooks 之间是平铺的, 能够定义更复杂的关系(依赖).

  • 更容易进行逻辑和视图分离. hooks 自然隔离 JSX, 视图和逻辑之间的界限比较清晰, 这使得 hooks 能够更专一组件的行为.

  • 淡化组件生命周期概念, 将原本分散在多个生命周期的相关逻辑聚合起来

  • 一点点'响应式编程'的味道, 每一个 hooks 都包含一些状态和反作用,这些数据能够在 hooks 之间传递流动和响应, 见下文

  • 跨平台的逻辑复用. 这是我本身开的脑洞, React hooks 出来以后尤雨溪就推了一个vue-hooks试验项目, 若是后面发展顺利, hooks 是可能被用于跨框架复用?


一个示例: 无限加载列表

Edit useList


通常 hooks 的基本代码结构为:

function useHook(options) {
  // ⚛️states
  const [someState, setSomeState] = useState(initialValue);
  // ⚛️derived state
  const computedState = useMemo(() => computed, [dependencies]);

  // ⚛️refs
  const refSomething = useRef();

  // ⚛️side effect
  useEffect(() => {}, []);
  useEffect(() => {}, [dependencies]);

  // ⚛️state operations
  const handleChange = useCallback(() => {
    setSomeState(newState)
  }, [])

  // ⚛️output
  return <div>{...}</div>
}
复制代码

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


一些注意事项:

  • 只能在组件顶层调用 hooks。不要在循环,控制流和嵌套的函数中调用 hooks
  • 只能从 React 的函数组件中调用 hooks
  • 自定义 hooks 使用 use*命名

总结 hooks 的经常使用场景:

  • 反作用封装和监听: 例如 useWindowSize(监听窗口大小),useOnlineStatus(在线状态)
  • 反作用衍生: useEffect, useDebounce, useThrottle, useTitle, useSetTimeout
  • DOM 事件封装:useActive,useFocus, useDraggable, useTouch
  • 获取 context
  • 封装可复用逻辑和状态: useInput, usePromise(异步请求), useList(列表加载)
    • 取代高阶组件和 render Props. 例如使用 useRouter 取代 withRouter, useSpring 取代旧的 Spring Render Props 组件
    • 取代容器组件
    • 状态管理器: use-global-hook, unstated
  • 扩展状态操做: 原始的 useState 很简单,因此有很大的扩展空间,例如 useSetState(模拟旧的 setState), useToggle(boolean 值切换),useArray, useLocalStorage(同步持久化到本地存储)
  • 继续开脑洞...: hooks 的探索还在继续

学习 hooks:




5. hooks 实现响应式编程

Vue的非侵入性响应式系统是其最独特的特性之一, 能够按照 Javascript 的数据操做习惯来操做组件状态, 而后自动响应到页面中. 而 React 这边则提供了 setState, 对于复杂的组件状态, setState 会让代码变得的又臭又长. 例如:

this.setState({
  pagination: {
    ...this.state.pagination,
    current: defaultPagination.current || 1,
    pageSize: defaultPagination.pageSize || 15,
    total: 0,
  },
});
复制代码

后来有了mobx, 基本接近了 Vue 开发体验:

@observer
class TodoView extends React.Component {
  private @observable loading: boolean;
  private @observable error?: Error;
  private @observable list: Item[] = [];
  // 衍生状态
  private @computed get completed() {
    return this.list.filter(i => i.completed)
  }

  public componentDidMount() {
    this.load();
  }

  public render() {
    /// ...
  }

  private async load() {
    try {
      this.error = undefined
      this.loading = true
      const list = await fetchList()
      this.list = list
    } catch (err) {
      this.error = err
    } finally {
      this.loading = false
    }
  }
}
复制代码

其实 mobx 也有挺多缺点:

  • 代码侵入性. 全部须要响应数据变更的组件都须要使用 observer 装饰, 属性须要使用 observable 装饰, 以及数据操做方式. 对 mobx 耦合较深, 往后切换框架或重构的成本很高

  • 兼容性. mobx v5 后使用 Proxy 进行重构, Proxy 在 Chrome49 以后才支持. 若是要兼容旧版浏览器则只能使用 v4, v4 有一些, 这些坑对于不了解 mobx 的新手很难发现:

    • Observable 数组并不是真正的数组. 好比 antd 的 Table 组件就不认 mobx 的数组, 须要传入到组件之间使用 slice 进行转换
    • 向一个已存在的 observable 对象中添加属性不会被自动捕获

因而 hooks 出现了, 它让组件的状态管理变得更简单直接, 并且它的思想也很接近 mobx 响应式编程哲学:

mobx


  1. 简洁地声明状态

状态 是驱动应用的数据. 例如 UI 状态或者业务领域状态

function Demo() {
  const [list, setList] = useState<Item[]>([]);
  // ...
}
复制代码
  1. 衍生

任何 源自状态而且不会再有任何进一步的相互做用的东西就是衍生。包括用户视图, 衍生状态, 其余反作用

function Demo(props: { id: string }) {
  const { id } = props;
  // 取代mobx的observable: 获取列表, 在挂载或id变更时请求
  const [value, setValue, loading, error, retry] = usePromise(
    async id => {
      return getList(id);
    },
    [id],
  );

  // 衍生状态: 取代mobx的computed
  const unreads = useMemo(() => value.filter(i => !i.readed), [value]);

  // 衍生反作用: value变更后自动持久化
  useDebounce(
    () => {
      saveList(id, value);
    },
    1000,
    [value],
  );

  // 衍生视图
  return <List data={value} onChange={setValue} error={error} loading={loading} retry={retry} />;
}
复制代码

因此说 hook 是一个革命性的东西, 它可让组件的状态数据流更加清晰. 换作 class 组件, 咱们一般的作法多是在 componentDidUpdate生命周期方法中进行数据比较, 而后命令式地触发一些方法. 好比 id 变化时触发 getList, list 变化时进行 saveList.

hook 彷佛在淡化组件生命周期的概念, 让开发者更专一于状态的关系, 以数据流的方式来思考组件的开发. Dan Abramov编写有弹性的组件也提到了一个原则"不要阻断数据流", 证明了笔者的想法:

不管什么时候使用 props 和 state,请考虑若是它们发生变化会发生什么。在大多数状况下,组件不该以不一样方式处理初始渲染和更新流程。这使它可以适应逻辑上的变化。

读者能够看一下awesome-react-hooks, 这些开源的 hook 方案都挺有意思. 例如rxjs-hooks, 巧妙地将 react hooks 和 rxjs 结合的起来:

function App(props: { foo: number }) {
  // 响应props的变更
  const value = useObservable(inputs$ => inputs$.pipe(map(([val]) => val + 1)), 200, [props.foo]);
  return <h1>{value}</h1>;
}
复制代码



6. 类继承也有用处

就如 react 官方文档说的: "咱们的 React 使用了数以千计的组件,然而却还未发现任何须要推荐你使用继承的状况。", React 偏向于函数式编程的组合模式, 面向对象的继承实际的应用场景不多.

当咱们须要将一些传统的第三方库转换成 React 组件库时, 继承就可能派上用场. 由于这些库大部分是使用面向对象的范式来组织的, 比较典型的就是地图 SDK. 以百度地图为例:

baidu overlay

百度地图有各类组件类型: controls, overlays, tileLayers. 这些类型都有多个子类, 如上图, overlay 有 Label, Marker, Polyline 等这些子类, 且这些子类有相同的生命周期, 都是经过 addOverlay 方法来渲染到地图画布上. 咱们能够经过继承的方式将他们生命周期管理抽取到父类上, 例如:

// Overlay抽象类, 负责管理Overlay的生命周期
export default abstract class Overlay<P> extends React.PureComponent<OverlayProps & P> {
  protected initialize?: () => void;
  // ...
  public componentDidMount() {
    // 子类在constructor或initialize方法中进行实例化
    if (this.initialize) {
      this.initialize();
    }

    if (this.instance && this.context) {
      // 渲染到Map画布中
      this.context.nativeInstance!.addOverlay(this.instance);
      // 初始化参数
      this.initialProperties();
    }
  }

  public componentDidUpdate(prevProps: P & OverlayProps) {
    // 属性更新
    this.updateProperties(prevProps);
  }

  public componentWillUnmount() {
    // 组件卸载
    if (this.instance && this.context) {
      this.context.nativeInstance!.removeOverlay(this.instance);
    }
  }
  // ...
  // 其余通用方法
  private forceReloadIfNeed(props: P, prevProps: P) {
    ...
  }
}
复制代码

子类的工做就变得简单不少, 声明本身的属性/事件和实例化具体类:

export default class Label extends Overlay<LabelProps> {
  public static defaultProps = {
    enableMassClear: true,
  };

  public constructor(props: LabelProps) {
    super(props);
    const { position, content } = this.props;
    // 声明支持的属性和回调
    this.extendedProperties = PROPERTIES;
    this.extendedEnableableProperties = ENABLEABLE_PROPERTIES;
    this.extendedEvents = EVENTS;

    // 实例化具体类
    this.instance = new BMap.Label(content, {
      position,
    });
  }
}
复制代码

代码来源于 react-bdmap

固然这个不是惟一的解决方法, 使用高阶组件和 hooks 一样可以实现. 只不过对于本来就采用面向对象范式组织的库, 使用继承方式会更加好理解




7. 模态框管理

modal demo

模态框是应用开发中使用频率很是高组件,尤为在中后台管理系统中. 可是在 React 中用着并非特别爽, 典型的代码以下:

const Demo: FC<{}> = props => {
  // ...
  const [visible, setVisible] = useState(false);
  const [editing, setEditing] = useState();
  const handleCancel = () => {
    setVisible(false);
  };

  const prepareEdit = async (item: Item) => {
    // 加载详情
    const detail = await loadingDeatil(item.id);
    setEditing(detail);
    setVisible(true);
  };

  const handleOk = async () => {
    try {
      const values = await form.validate();
      // 保存
      await save(editing.id, values);
      // 隐藏
      setVisible(false);
    } catch {}
  };

  return;
  <>
    <Table
      dataSource={list}
      columns={[
        {
          text: '操做',
          render: item => {
            return <a onClick={() => prepareEdit(item)}>编辑</a>;
          },
        },
      ]}
    />
    <Modal visible={visible} onOk={handleOk} onCancel={handleHide}>
      {/* 表单渲染 */}
    </Modal>
  </>;
};
复制代码

上面的代码太丑了, 不相关逻辑堆积在一个组件下 ,不符合单一职责. 因此咱们要将模态框相关代码抽取出去, 放到EditModal中:

const EditModal: FC<{ id?: string; visible: boolean; onCancel: () => void; onOk: () => void }> = props => {
  // ...
  const { visible, id, onHide, onOk } = props;
  const detail = usePromise(async (id: string) => {
    return loadDetail(id);
  });

  useEffect(() => {
    if (id != null) {
      detail.call(id);
    }
  }, [id]);

  const handleOk = () => {
    try {
      const values = await form.validate();
      // 保存
      await save(editing.id, values);
      onOk();
    } catch {}
  };

  return (
    <Modal visible={visible} onOk={onOk} onCancel={onCancel}>
      {detail.value &&
        {
          /* 表单渲染 */
        }}
    </Modal>
  );
};

/**
 * 使用
 */
const Demo: FC<{}> = props => {
  // ...
  const [visible, setVisible] = useState(false);
  const [editing, setEditing] = useState<string | undefined>(undefined);
  const handleHide = () => {
    setVisible(false);
  };

  const prepareEdit = async (item: Item) => {
    setEditing(item.id);
    setVisible(true);
  };

  return;
  <>
    <Table
      dataSource={list}
      columns={[
        {
          text: '操做',
          render: item => {
            return <a onClick={() => prepareEdit(item)}>编辑</a>;
          },
        },
      ]}
    />
    <EditModal id={editing} visible={visible} onOk={handleHide} onCancel={handleHide}>
      {' '}
    </EditModal>
  </>;
};
复制代码

如今编辑相关的逻辑抽取到了 EditModal 上,可是 Demo 组件还要维护模态框的打开状态和一些数据状态。一个复杂的页面可能会有不少模态框,这样的代码会变得愈来愈恶心, 各类 xxxVisible 状态满天飞. 从实际开发角度上将,模态框控制的最简单的方式应该是这样的:

const handleEdit = item => {
  EditModal.show({
    // 🔴 经过函数调用的方式出发弹窗. 这符合对模态框的习惯用法, 不关心模态框的可见状态. 例如window.confirm, wx.showModal().
    id: item.id, // 🔴 传递数据给模态框
    onOk: saved => {
      // 🔴 事件回调
      refreshList(saved);
    },
    onCancel: async () => {
      return confirm('确认取消'); // 控制模态框是否隐藏
    },
  });
};
复制代码

这种方式在社区上也是有争议的,有些人认为这是 React 的反模式,@欲三更Modal.confirm 违反了 React 的模式吗?就探讨了这个问题。 以图为例:

modal confirm
图片一样出自欲三更文章

红线表示时间驱动(或者说时机驱动), 蓝线表示数据驱动。欲三更认为“哪怕一个带有明显数据驱动特点的 React 项目,也存在不少部分不是数据驱动而是事件驱动的. 数据只能驱动出状态,只有时机才能驱动出行为, 对于一个时机驱动的行为,你非得把它硬坳成一个数据驱动的状态,你不以为很奇怪吗?”. 他的观点正不正确笔者不作评判, 可是某些场景严格要求‘数据驱动’,可能会有不少模板代码,写着会很难受.

So 怎么实现?

能够参考 antd Modal.confirm的实现, 它使用ReactDOM.render来进行外挂渲染,也有人使用Context API来实现的. 笔者认为比较接近理想的(至少 API 上看)是react-comfirm这样的:

/**
 * EditModal.tsx
 */
import { confirmable } from 'react-confirm';
const EditModal = props => {
  /*...*/
};

export default confirmable(EditModal);

/**
 *  Demo.tsx
 */
import EditModal from './EditModal';

const showEditModal = createConfirmation(EditModal);

const Demo: FC<{}> = props => {
  const prepareEdit = async (item: Item) => {
    showEditModal({
      id: item.id, // 🔴 传递数据给模态框
      onOk: saved => {
        // 🔴 事件回调
        refreshList(saved);
      },
      onCancel: async someValues => {
        return confirm('确认取消'); // 控制模态框是否隐藏
      },
    });
  };

  // ...
};
复制代码

使用ReactDOM.render外挂渲染形式的缺点就是没法访问 Context,因此仍是要妥协一下,结合 Context API 来实现示例:

Edit useModal

扩展




8. 使用 Context 进行依赖注入

Context 为组件树提供了一个传递数据的方法,从而避免了在每个层级手动的传递 props 属性.

Context 在 React 应用中使用很是频繁, 新的Context API也很是易用. Context 经常使用于如下场景:

  • 共享那些被认为对于一个'组件树'而言是“全局”的数据. 如当前认证的用户, 主题, i18n 配置, 表单状态
  • 组件配置. 配置组件的行为, 如 antd 的 ConfigProvider
  • 跨组件通讯. 不推荐经过'事件'进行通讯, 而是经过'状态'进行通讯
  • 依赖注入
  • 状态管理器. Context 通过一些封装能够基本取代 Redux 和 Mobx 这些状态管理方案. 后续有专门文章介绍

Context 的做用域是子树, 也就是说一个 Context Provider 能够应用于多个子树, 子树的 Provider 也能够覆盖父级的 Provider 的 value. 基本结构:

import React, {useState, useContext} from 'react'

export inteface MyContextValue {
  state: number
  setState: (state: number) => void
}

const MyContext = React.createContext<MyContextValue>(
  {
    state: 1,
    // 设置默认值, 抛出错误, 必须配合Provider使用
    setState: () => throw new Error('请求MyContextProvider组件下级调用')
  }
)

export const MyContextProvider: FC<{}> = props => {
  const [state, setState] = useState(1)
  return <MyContext.Provider value={{state, setState}}>{props.children}</MyContext.Provider>
}

export function useMyContext() {
  return useContext(MyContext)
}

export default MyContextProvider
复制代码

Context 默认值中的方法应该抛出错误, 警告不规范的使用

扩展:




9. 不可变的状态

对于函数式编程范式的 React 来讲,不可变状态有重要的意义.

  • 不可变数据具备可预测性。可不变数据可让应用更好调试,对象的变动更容易被跟踪和推导.

    就好比 Redux, 它要求只能经过 dispatch+reducer 进行状态变动,配合它的 Devtool 能够很好的跟踪状态是如何被变动的. 这个特性对于大型应用来讲意义重大,由于它的状态很是复杂,若是不加以组织和约束,你不知道是哪一个地方修改了状态, 出现 bug 时很难跟踪.

    因此说对于严格要求单向数据流的状态管理器(Redux)来讲,不可变数据是基本要求,它要求整个应用由一个单一的状态进行映射,不可变数据可让整个应用变得可被预测.

  • 不可变数据还使一些复杂的功能更容易实现。避免数据改变,使咱们可以安全保留对旧数据的引用,能够方便地实现撤销重作,或者时间旅行这些功能

  • 能够精确地进行从新渲染判断。能够简化 shouldComponentUpdate 比较。

实现不可变数据的流行方法:

笔者比较喜欢 immer,没有什么心智负担, 按照 JS 习惯的对象操做方式就能够实现不可变数据。




10. React-router: URL 即状态

传统的路由主要用于区分页面, 因此一开始前端路由设计也像后端路由(也称为静态路由)同样, 使用对象配置方式, 给不一样的 url 分配不一样的页面组件, 当应用启动时, 在路由配置表中查找匹配 URL 的组件并渲染出来.

React-Router v4 算是一个真正意义上符合组件化思惟的路由库, React-Router 官方称之为‘动态路由’, 官方的解释是"指的是在应用程序渲染时发生的路由,而不是在运行应用程序以外的配置或约定中发生的路由", 具体说, <Route/>变成了一个普通 React 组件, 它在渲染时判断是否匹配 URL, 若是匹配就渲染指定的组件, 不匹配就返回 null.

这时候 URL 意义已经不同了, URL 再也不是简单的页面标志, 而是应用的状态; 应用构成也再也不局限于扁平页面, 而是多个能够响应 URL 状态的区域(可嵌套). 由于思惟转变很大, 因此它刚出来时并不受青睐. 这种方式更加灵活, 因此选择 v4 不表明放弃旧的路由方式, 你彻底能够按照旧的方式来实现页面路由.

举个应用实例: 一个应用由三个区域组成: 侧边栏放置多个入口, 点击这些入口会加载对应类型的列表, 点击列表项须要加载详情. 三个区域存在级联关系

router demo

首先设计可以表达这种级联关系的 URL, 好比/{group}/{id}, URL 设计通常遵循REST 风格, 那么应用的大概结构是这样子:

// App
const App = () => {
  <div className="app">
    <SideBar />
    <Route path="/:group" component={ListPage} />
    <Route path="/:group/:id" component={Detail} />
  </div>;
};

// SideBar
const Sidebar = () => {
  return (
    <div className="sidebar">
      {/* 使用NavLink 在匹配时显示激活状态 */}
      <NavLink to="/message">消息</NavLink>
      <NavLink to="/task">任务</NavLink>
      <NavLink to="/location">定位</NavLink>
    </div>
  );
};

// ListPage
const ListPage = props => {
  const { group } = props.match.params;
  // ...

  // 响应group变化, 并加载指定类型列表
  useEffect(() => {
    load(group);
  }, [group]);

  // 列表项也会使用NavLink, 用于匹配当前展现的详情, 激活显示
  return <div className="list">{renderList()}</div>;
};

// DetailPage
const DetailPage = props => {
  const { group, id } = props.match.params;
  // ...

  // 响应group和id, 并加载详情
  useEffect(() => {
    loadDetail(group, id);
  }, [group, id]);

  return <div className="detail">{renderDetail()}</div>;
};
复制代码

扩展




11. 组件规范

扩展

相关文章
相关标签/搜索