React Hook起飞指南

2019-08-15 admin

作者:元潇 方凳雅集出品

16.8目前放出来了10个内置hook,但仅仅基于以下两个API,就能做很多事情。所以这篇文章不会讲很多API,也不会讲API的基本用法,只把这两个能做的事情讲清楚,阅读全文大概5-10分钟。

  • 状态管理:useState
  • 副作用管理:useEffect

这两个api就是hook世界里的镰刀和锤子,看似简单的两个api实际上所代表的,是相比以前截然不同的一种新的编程模型。

前言:已经有了class component,为什么又来了一个hook?

Dan在他的博客上提到:

我们知道组件和自上而下的数据流可以帮助我们将大型UI组织成小型,独立,可重用的部分。 但是,我们经常无法进一步破坏复杂组件,因为逻辑是有状态的,无法提取到函数或其他组件中。而hook让我们可以将组件内部的逻辑组织成可重用的隔离单元。

所以,一句话总结hook带来的变革就是:将可复用的最小单元从组件层面进一步细化到逻辑层面。

基于这一点优势,在后面我们看可以看到,基于hook开发的应用里的所有组件都不会随业务增长而变得臃肿,因为在hook的世界里,状态逻辑和UI是解耦的。UI只需要消费最终计算出来的状态数据,hook只关注状态的计算和改变。

一、有状态的函数

useState组件是有状态的:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

函数是无状态的:

const Example = props => {
  const { count, onClick } = props;
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={onClick}>
        Click me
      </button>
    </div>
  );
}

hooks是有状态的函数:

import { useState } from 'react';
const Example = () => {
    const [count, setCount] = useState(0);
    return (
      <div>
        <p>You clicked {count} times</p>
        <button onClick={() => setCount(count + 1)}>
          Click me
        </button>
      </div>
    );
}

Think: useState生产出来的setter在更新state的时候不会合并,这点不同于传统class组件的setState方法,为什么这样设计?

// Don't do such like this ↓
const [data, setData] = useState({ count: 0, name: 'zby' });
useEffect(() => {
  // data: { count: 0, name: 'zby' } -> { count: 0 }
  setData({ count: 1 });
}, []);

我们的应用都是从小到大发展起来的,初始充分的组件划分和状态设计是保证应用后续可维护性的重要一环,因为随着应用的扩增,组件难免变得臃肿。所以有时我们也从一开始就一步到位引入redux之类的状态管理。

但现在,在我们的“纯函数”组件里,每个useState都会生产出一对儿state和stateSetter,我们无需考虑更多的状态树的设计和组件的划分设计,逻辑代码直接从根组件写起,渐进式开发变得可行

所谓“渐进式”开发:概括应用的发展路径大致可以分为以下3个阶段:

1. 前期farm: 只需要把相关 state 组合到几个独立的 state 变量即可应付绝大多数情况;

**2.中期gank:**当组件的状态逐渐变得多起来时,我们可以很轻松地将状态的更新交给reducer来管理(详情在下文第二章展开);

**3.大后期团战:**不光状态多了,状态的逻辑也越来越复杂的时候,我们可以几乎0成本的将繁杂的状态逻辑代码抽离成自定义hook解决问题(详情在下文第三章展开);

基于hook,我们的开发过程,变得比以往更有弹性。所以这样的渐进式的开发变得可行且高效。

二、 高度灵活的redux,纯粹、无依赖

上文说道,当组件的状态逐渐变得多起来时,我们可以很自然的将状态的更新交给reducer来管理。

不同于真正的redux,在实际应用中,hook带来了一种更加灵活和纯粹的模式。现在我们可以用10行代码实现一个全局的redux,也可以用2行代码随时随地实现一个局部的redux。

A:10行代码实现一个全局redux:

import React from 'react';
const store = React.createContext(null);
export const initialState = { name: '元潇' };
export function reducer(state, action) {
  switch (action.type) {
    case 'changeName': return { ...state, name: action.payload };
    default: throw new Error('Unexpected action');
  }
}
export default store;

Provider根组件挂上即可

import React, { useReducer } from 'react';
import store, { reducer, initialState } from './store';
function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
     <store.Provider value={{ state, dispatch }}>
      <div/>
     </store>
  )
}

子组件调用

import React, { useContext } from 'react';
import store from './store';
function Child() {
  const { state, dispatch } = useContext(store);
  ...
}

B:随时随地实现一个局部redux

import React, { useReducer } from 'react';
const initialState = { name: '元潇' };
function reducer(state, action) {
  switch (action.type) {
    case 'changeName': return { ...state, name: action.payload };
    default: throw new Error('Unexpected action');
  }
}
function Component() {
  const [state, dispatch] = useReducer(reducer, initialState);
  ...
}
  • useState的本质是useReducer的一个语法糖,感兴趣可以阅读一下hooks的类型定义和实现。

三、自定义hook

上上文说道,当组件发展到一定程度,不光是状态多了,状态的逻辑也越来越复杂的时候,我们可以几乎0成本的将繁杂的状态逻辑代码抽离成自定义hook解决问题。

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。而组件和 hook 都是函数,所以也同样适用这种方式。不同的是,hook 是有状态的函数,它能实现以往纯函数所不能做到的更高级别的复用——状态逻辑的复用。

且先看下面两个demo示例

A:class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      name: undefined
    };
  }
  componentDidMount() {
    service.getInitialCount().then(data => {
      this.setState({ count: data });
    });
    service.getInitialName().then(data => {
      this.setState({ name: data });
    });
  }
  componentWillUnmount() {
    service.finishCounting().then(() => {
      alert('计数完成');
    });
  }
  addCount = () => {
    this.setState({ count: this.state.count + 1 });
  };
  handleNameChange = name => {
    this.setState({ name });
  };
  render() {
    const { count, name } = this.state;
    return (
      <div>
        <div>
          <p>You clicked {count} times</p>
          <button onClick={this.addCount}>Click me</button>
        </div>
        <Input value={name} onChange={this.handleNameChange} />
      </div>
    );
  }
}
B:function useCount(initialValue) {
  const [count, setCount] = useState(initialValue);
  useEffect(() => {
    service.getInitialCount().then(data => {
      setCount(data);
    });
    return () => {
      service.finishCounting().then(() => {
        alert('计数完成');
      });
    };
  }, []);
  function addCount() {
    setCount(c => c + 1);
  }
  return { count, addCount };
}
function useName(initialValue) {
  const [name, setName] = useState(initialValue);
  useEffect(() => {
    service.getInitialName().then(data => {
      setName(data);
    });
  }, []);
  function handleNameChange(value) {
    setName(value);
  }
  return { name, handleNameChange };
}
const App = () => {
  const { count, addCount } = useCount(0);
  const { name, setName } = useName();
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={addCount}>Click me</button>
      <Input value={name} onChange={setName} />
    </div>
  );
};

A有2个状态:count和name,还有与之相关的一票儿逻辑,散落在组件的生命周期和方法里。虽然我们可以将组件的state和变更action抽成公共的,但涉及到副作用的action,到最终还是绕不开组件的生命周期。但一个组件的生命周期只有一套,不可避免的会出现一些完全不相干的逻辑写在一起。如此一来,便无法实现完全的状态逻辑复用。

在B中,我们将count相关的逻辑和name相关的逻辑通过自定义hook,封装在独立且封闭的逻辑单元里。以往class组件的生命周期在这里不复存在。生命周期是和UI强耦合的一个概念,虽然易于理解,但它天然距离数据很遥远。而hook以一种类似rxjs模式的数据流订阅实现了组件的副作用封装,这样的好处就是我们只需要关心数据。所以hook所带来的,绝不仅仅只是简化了state的定义与包装。

这个动画,很好的展示了A->B前后相关联的状态逻辑的组织方式变化。

业务实战记录:

这是一个商品详情页用到的SKU选择组件

我们使用hook将原有的class组件进行重构,这个重构的过程就是一个状态逻辑的抽取过程。

我们将组件核心的2个状态和相关的逻辑,抽到了2个独立的自定义hook中(见下图)。这样做的好处很明显,我们的组件只需要去消费这两个hook产出的value和function,状态的维护和更新细节,已经被封装在hook里。

const { specPath, handleSpecPathChange } = useSkuSpecPath({
    defaultSpecPath,
    dataSource
  });
const { skus, handleSkuAmountChange } = useSkuAmount({
  defaultSelectedSkus,
  dataSource
});

在这里提出一个自定义hook的设计范式:

const { state, handleChange, others } = useCustomHook(config, dependency?);

其中config声明了hook所需要的数据,可能是内部useState的初始值,也可能是结构化的数据,总结起来就是这个hook的配置

dependency通常只有hook内使用了useEffect、useCallback这类API,需要我们声明依赖的时候需要传入。

左边是重构后的代码(脱敏代码,领会精神就好),原先核心的通用逻辑被抽到useSkuSpecPath和useSkuAmount这两个hook里后,这个组件变成了它们的调用方。后续不管UI组件如何变化和扩展,只要符合它们的接口格式约定,就可以随时随地地复用这些逻辑,很快地实现一个新的sku选择组件。

总结:自定义hook实现了状态逻辑与UI分离,通过合理抽象自定义hook,能够实现非常高级别的业务逻辑抽象复用。

推荐一个网站,里面收集了一些有意思的自定义hook

四、未来引用Dan的一句话:

hook可以涵盖class组件的所有使用场景,同时在抽取、测试和重用代码方面提供了更大的灵活性。这就是为什么Hooks代表了我们对React未来的愿景。

react16.8以上的应用里,大家可以立马用起来了。

彩蛋

hook原理: Not magic,just array

let hooks, i;
function useState() {
  i++;
  if (hooks[i]) {
    // 再次渲染时
    return hooks[i];
  }
  // 第一次渲染
  hooks.push(...);
}
// 准备渲染
i = -1;
hooks = fiber.hooks || [];
// 调用组件
Component();
// 缓存 Hooks 的状态
fiber.hooks = hooks;

[转载]原文链接:https://segmentfault.com/a/1190000020078158

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-72561.html

文章标题:React Hook起飞指南

相关文章
vue下使用 pdf.js 预览 和 打印 PDF文档 兼容React
我使用前端开发框架是vue,有一个打印PDF文档的需求. 这个需求最开始是交给后台,但是明显不切实际, 因为后台服务器,根本就无法连接打印机. 所以这个预览加打印文档的需求就交到了前端, 开始我有想过直接打开pdf文件, 但事实上直接打开p...
2018-06-06
一个适合新手的react小demo
之前一直都是在用vue,这两个月换了个公司,不过现在又离职了,这里什么框架都用, vue、angular、react,一般公司都是选二项技术去开发已经就够了, 要不是vue+angular或是react+angular个人觉得用了vue还来...
2018-01-11
webpack入门+react环境配置
本文介绍了react.js使用webpack搭配环境的入门教程,分享给大家,也给自己做个笔记 如果你想直接上手开发,而跳过这些搭配环境的繁琐过程,推荐你使用官方的create-react-app命令 npm install -g creat...
2017-03-20
#react-native# Error: Command failed: gradlew.bat installDebug
这篇文章主要解决react-native中遇到的bug。 编译并运行 React Native 应用之前我们可以使用下面的方法清理gradlew 和之前的构建文件。 Error: Command failed: gradlew.bat in...
2019-02-28
React-Navigation 导航综合应用
本文由我们团队的 杨俊宁 组内分享后总结。 移动应用中最常见的导航风格可能是基于 tab 的导航。 可以在屏幕底部或顶部的标题栏下方(甚至取代标题栏)。 createStackNavigator 创建的 stack navigator...
2018-06-27
React水印组件,支持图片水印,文字水印
React水印组件,支持图片水印,文字水印。 安装 npm i --save react-watermark-module 用法 import ReactWatermark from &#x27;react-watermark-modul...
2018-01-09
javascript中setTimeout使用指南
javascript中setTimeout使用指南 &lt;script&gt; &#x2F;* &#x2F;&#x2F;方法1 function slows(){ alert(&quot;15S后弹出!&quot;); } setTi...
2017-03-27
10分钟让你的flutter程序拥有定位功能--集成高德地图定位指南
地图定位这个功能目前基本上是商业应用app的标配。然而,在flutter中进行原生功能的开发,意味着必须的ios和android双端都通,而且需要大量的调试时间。尤其目前这个时间点,flutter的版本更新频繁,原生编译问题重重。不过没关系...
2018-09-11
跟着例子一步步学习redux+react-redux
前言 本文不会拿redux、react-redux等一些react的名词去讲解,然后把各自用法举例说明,这样其实对一些react新手或者不太熟悉redux模式的开发人员不够友好,他们并不知道这样使用的原因。本文通过一个简单的例子展开,一点点...
2018-01-26
使用 React 和 Django REST Framework 构建你的网站
此专栏我会不定期分享一些 Django 最前沿的文章,内容偏重技巧、经验的归纳总结,来源暂时有: Medium Twitter 知名博主 如果大家感兴趣,请一定点个关注,给我一些动力,毕竟翻译整理是需要时间的,谢谢大家 – 原文地址:...
2018-02-08
回到顶部