资讯专栏INFORMATION COLUMN

精读《React Hooks》

kohoh_ / 2148人阅读

摘要:更容易将组件的与状态分离。也就是只提供状态处理方法,不会持久化状态。大体思路是利用共享一份数据,作为的数据源。精读带来的约定函数必须以命名开头,因为这样才方便做检查,防止用判断包裹语句。前端精读帮你筛选靠谱的内容。

1 引言

React Hooks 是 React 16.7.0-alpha 版本推出的新特性,想尝试的同学安装此版本即可。

React Hooks 要解决的问题是状态共享,是继 render-props 和 higher-order components 之后的第三种状态共享方案,不会产生 JSX 嵌套地狱问题。

状态共享可能描述的不恰当,称为状态逻辑复用会更恰当,因为只共享数据处理逻辑,不会共享数据本身。

不久前精读分享过的一篇 Epitath 源码 - renderProps 新用法 就是解决 JSX 嵌套问题,有了 React Hooks 之后,这个问题就被官方正式解决了。

为了更快理解 React Hooks 是什么,先看笔者引用的下面一段 renderProps 代码:

function App() {
  return (
    
      {({ on, toggle }) => (
        
        
      )}
    
  )
}

恰巧,React Hooks 解决的也是这个问题:

function App() {
  const [open, setOpen] = useState(false);
  return (
    <>
      
       setOpen(false)}
        onCancel={() => setOpen(false)}
      />
    
  );
}

可以看到,React Hooks 就像一个内置的打平 renderProps 库,我们可以随时创建一个值,与修改这个值的方法。看上去像 function 形式的 setState,其实这等价于依赖注入,与使用 setState 相比,这个组件是没有状态的

2 概述

React Hooks 带来的好处不仅是 “更 FP,更新粒度更细,代码更清晰”,还有如下三个特性:

多个状态不会产生嵌套,写法还是平铺的(renderProps 可以通过 compose 解决,可不但使用略为繁琐,而且因为强制封装一个新对象而增加了实体数量)。

Hooks 可以引用其他 Hooks。

更容易将组件的 UI 与状态分离。

第二点展开说一下:Hooks 可以引用其他 Hooks,我们可以这么做:

import { useState, useEffect } from "react";

// 底层 Hooks, 返回布尔值:是否在线
function useFriendStatusBoolean(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

// 上层 Hooks,根据在线状态返回字符串:Loading... or Online or Offline
function useFriendStatusString(props) {
  const isOnline = useFriendStatusBoolean(props.friend.id);

  if (isOnline === null) {
    return "Loading...";
  }
  return isOnline ? "Online" : "Offline";
}

// 使用了底层 Hooks 的 UI
function FriendListItem(props) {
  const isOnline = useFriendStatusBoolean(props.friend.id);

  return (
    
  • {props.friend.name}
  • ); } // 使用了上层 Hooks 的 UI function FriendListStatus(props) { const statu = useFriendStatusString(props.friend.id); return
  • {statu}
  • ; }

    这个例子中,有两个 Hooks:useFriendStatusBooleanuseFriendStatusString, useFriendStatusString 是利用 useFriendStatusBoolean 生成的新 Hook,这两个 Hook 可以给不同的 UI:FriendListItemFriendListStatus 使用,而因为两个 Hooks 数据是联动的,因此两个 UI 的状态也是联动的。

    顺带一提,这个例子也可以用来理解 对 React Hooks 的一些思考 一文的那句话:“有状态的组件没有渲染,有渲染的组件没有状态”

    useFriendStatusBooleanuseFriendStatusString 是有状态的组件(使用 useState),没有渲染(返回非 UI 的值),这样就可以作为 Custom Hooks 被任何 UI 组件调用。

    FriendListItemFriendListStatus 是有渲染的组件(返回了 JSX),没有状态(没有使用 useState),这就是一个纯函数 UI 组件,

    利用 useState 创建 Redux

    Redux 的精髓就是 Reducer,而利用 React Hooks 可以轻松创建一个 Redux 机制:

    // 这就是 Redux
    function useReducer(reducer, initialState) {
      const [state, setState] = useState(initialState);
    
      function dispatch(action) {
        const nextState = reducer(state, action);
        setState(nextState);
      }
    
      return [state, dispatch];
    }

    这个自定义 Hook 的 value 部分当作 redux 的 state,setValue 部分当作 redux 的 dispatch,合起来就是一个 redux。而 react-redux 的 connect 部分做的事情与 Hook 调用一样:

    // 一个 Action
    function useTodos() {
      const [todos, dispatch] = useReducer(todosReducer, []);
    
      function handleAddClick(text) {
        dispatch({ type: "add", text });
      }
    
      return [todos, { handleAddClick }];
    }
    
    // 绑定 Todos 的 UI
    function TodosUI() {
      const [todos, actions] = useTodos();
      return (
        <>
          {todos.map((todo, index) => (
            
    {todo.text}
    ))} ); }

    useReducer 已经作为一个内置 Hooks 了,在这里可以查阅所有 内置 Hooks。

    不过这里需要注意的是,每次 useReducer 或者自己的 Custom Hooks 都不会持久化数据,所以比如我们创建两个 App,App1 与 App2:

    function App1() {
      const [todos, actions] = useTodos();
    
      return todo count: {todos.length};
    }
    
    function App2() {
      const [todos, actions] = useTodos();
    
      return todo count: {todos.length};
    }
    
    function All() {
      return (
        <>
          
          
        
      );
    }

    这两个实例同时渲染时,并不是共享一个 todos 列表,而是分别存在两个独立 todos 列表。也就是 React Hooks 只提供状态处理方法,不会持久化状态。

    如果要真正实现一个 Redux 功能,也就是全局维持一个状态,任何组件 useReducer 都会访问到同一份数据,可以和 useContext 一起使用。

    大体思路是利用 useContext 共享一份数据,作为 Custom Hooks 的数据源。具体实现可以参考 redux-react-hook。

    利用 useEffect 代替一些生命周期

    在 useState 位置附近,可以使用 useEffect 处理副作用:

    useEffect(() => {
      const subscription = props.source.subscribe();
      return () => {
        // Clean up the subscription
        subscription.unsubscribe();
      };
    });

    useEffect 的代码既会在初始化时候执行,也会在后续每次 rerender 时执行,而返回值在析构时执行。这个更多带来的是便利,对比一下 React 版 G2 调用流程:

    class Component extends React.PureComponent {
      private chart: G2.Chart = null;
      private rootDomRef: React.ReactInstance = null;
    
      componentDidMount() {
        this.rootDom = ReactDOM.findDOMNode(this.rootDomRef) as HTMLDivElement;
    
        this.chart = new G2.Chart({
          container: document.getElementById("chart"),
          forceFit: true,
          height: 300
        });
        this.freshChart(this.props);
      }
    
      componentWillReceiveProps(nextProps: Props) {
        this.freshChart(nextProps);
      }
    
      componentWillUnmount() {
        this.chart.destroy();
      }
    
      freshChart(props: Props) {
        // do something
        this.chart.render();
      }
    
      render() {
        return 
    (this.rootDomRef = ref)} />; } }

    用 React Hooks 可以这么做:

    function App() {
      const ref = React.useRef(null);
      let chart: G2.Chart = null;
    
      React.useEffect(() => {
        if (!chart) {
          chart = new G2.Chart({
            container: ReactDOM.findDOMNode(ref.current) as HTMLDivElement,
            width: 500,
            height: 500
          });
        }
    
        // do something
        chart.render();
    
        return () => chart.destroy();
      });
    
      return 
    ; }

    可以看到将细碎的代码片段结合成了一个完整的代码块,更维护。

    现在介绍了 useState useContext useEffect useRef 等常用 hooks,更多可以查阅:内置 Hooks,相信不久的未来,这些 API 又会成为一套新的前端规范。

    3 精读 Hooks 带来的约定

    Hook 函数必须以 "use" 命名开头,因为这样才方便 eslint 做检查,防止用 condition 判断包裹 useHook 语句。

    为什么不能用 condition 包裹 useHook 语句,详情可以见 官方文档,这里简单介绍一下。

    React Hooks 并不是通过 Proxy 或者 getters 实现的(具体可以看这篇文章 React hooks: not magic, just arrays),而是通过数组实现的,每次 useState 都会改变下标,如果 useState 被包裹在 condition 中,那每次执行的下标就可能对不上,导致 useState 导出的 setter 更新错数据。

    虽然有 eslint-plugin-react-hooks 插件保驾护航,但这第一次将 “约定优先” 理念引入了 React 框架中,带来了前所未有的代码命名和顺序限制(函数命名遭到官方限制,JS 自由主义者也许会暴跳如雷),但带来的便利也是前所未有的(没有比 React Hooks 更好的状态共享方案了,约定带来提效,自由的代价就是回到 renderProps or HOC,各团队可以自行评估)。

    笔者认为,React Hooks 的诞生,也许来自于这个灵感:“不如通过增加一些约定,彻底解决状态共享问题吧!”

    React 约定大于配置脚手架 nextjs umi 以及笔者的 pri 都通过有 “约定路由” 的功能,大大降低了路由配置复杂度,那么 React Hooks 就像代码级别的约定,大大降低了代码复杂度。
    状态与 UI 的界限会越来越清晰

    因为 React Hooks 的特性,如果一个 Hook 不产生 UI,那么它可以永远被其他 Hook 封装,虽然允许有副作用,但是被包裹在 useEffect 里,总体来说还是挺函数式的。而 Hooks 要集中在 UI 函数顶部写,也很容易养成书写无状态 UI 组件的好习惯,践行 “状态与 UI 分开” 这个理念会更容易。

    不过这个理念稍微有点蹩脚的地方,那就是 “状态” 到底是什么。

    function App() {
      const [count, setCount] = useCount();
      return {count};
    }

    我们知道 useCount 算是无状态的,因为 React Hooks 本质就是 renderProps 或者 HOC 的另一种写法,换成 renderProps 就好理解了:

    {(count, setCount) => };
    
    function App(props) {
      return {props.count};
    }

    可以看到 App 组件是无状态的,输出完全由输入(Props)决定。

    那么有状态无 UI 的组件就是 useCount 了:

    function useCount() {
      const [count, setCount] = useState(0);
      return [count, setCount];
    }

    有状态的地方应该指 useState(0) 这句,不过这句和无状态 UI 组件 App 的 useCount() 很像,既然 React 把 useCount 成为自定义 Hook,那么 useState 就是官方 Hook,具有一样的定义,因此可以认为 useCount 是无状态的,useState 也是一层 renderProps,最终的状态其实是 useState 这个 React 内置的组件。

    我们看 renderProps 嵌套的表达:

    
      {(count, setCount) => (
        
          {" "}
          {/**虽然是透传,但给 count 做了去重,不可谓没有作用 */}
          {(count, setCount) => }
        
      )}
    

    能确定的是,App 一定有 UI,而上面两层父级组件一定没有 UI。为了最佳实践,我们尽量避免 App 自己维护状态,而其父级的 RenderProps 组件可以维护状态(也可以不维护状态,做个二传手)。因此可以考虑在 “有状态的组件没有渲染,有渲染的组件没有状态” 这句话后面加一句:没渲染的组件也可以没状态。

    4 总结

    把 React Hooks 当作更便捷的 RenderProps 去用吧,虽然写法看上去是内部维护了一个状态,但其实等价于注入、Connect、HOC、或者 renderProps,那么如此一来,使用 renderProps 的门槛会大大降低,因为 Hooks 用起来实在是太方便了,我们可以抽象大量 Custom Hooks,让代码更加 FP,同时也不会增加嵌套层级。

    5 更多讨论
    讨论地址是:精读《React Hooks》 · Issue #111 · dt-fe/weekly
    

    如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/99188.html

    相关文章

    • 精读《Vue3.0 Function API》

      摘要:拿到的都是而不是原始值,且这个值会动态变化。精读对于的与,笔者做一些对比。因此采取了作为优化方案只有当第二个依赖参数变化时才返回新引用。不需要使用等进行性能优化,所有性能优化都是自动的。前端精读帮你筛选靠谱的内容。 1. 引言 Vue 3.0 的发布引起了轩然大波,让我们解读下它的 function api RFC 详细了解一下 Vue 团队是怎么想的吧! 首先官方回答了几个最受关注的...

      voyagelab 评论0 收藏0
    • 精读《Function VS Class 组件》

      摘要:未来可能成为官方之一。讨论地址是精读组件如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读帮你筛选靠谱的内容。 1. 引言 为什么要了解 Function 写法的组件呢?因为它正在变得越来越重要。 那么 React 中 Function Component 与 Class Component 有何不同? how-are-function-components-di...

      FWHeart 评论0 收藏0
    • 精读《怎么用 React Hooks 造轮子》

      摘要:可以看到,这样不仅没有占用组件自己的,也不需要手写回调函数进行处理,这些处理都压缩成了一行。效果通过拿到周期才执行的回调函数。实现等价于的回调仅执行一次时,因此直接把回调函数抛出来即可。 1 引言 上周的 精读《React Hooks》 已经实现了对 React Hooks 的基本认知,也许你也看了 React Hooks 基本实现剖析(就是数组),但理解实现原理就可以用好了吗?学的是...

      Shihira 评论0 收藏0
    • 理解 React Hooks 的 Capture Value 特性

      摘要:在读了一些文章后,大致是找到自己总是掉坑的原因了没理解中的特性。通过这个示例,相信会比较容易地理解特性,并如何使用来暂时绕过它。在知道并理解这个特性后,有助于进一步熟悉了的运行机制,减少掉坑的次数。 由于刚使用 React hooks 不久,对它的脾气还拿捏不准,掉了很多次坑;这里的 坑 的意思并不是说 React hooks 的设计有问题,而是我在使用的时候,因为还没有跟上它的理念导...

      curlyCheng 评论0 收藏0
    • 精读react-easy-state 源码》

      摘要:会自动触发函数内回调函数的执行。因此利用并将依赖置为使代码在所有渲染周期内,只在初始化执行一次。同时代码里还对等公共方法进行了包装,让这些回调函数中自带效果。前端精读帮你筛选靠谱的内容。 1. 引言 react-easy-state 是个比较有趣的库,利用 Proxy 创建了一个非常易用的全局数据流管理方式。 import React from react; import { stor...

      curlyCheng 评论0 收藏0

    发表评论

    0条评论

    最新活动
    阅读需要支付1元查看
    <