React Hooks:初探·实践

前言

这篇文章主要介绍了React Hooks的一些实践用法和场景,遵循我我的一向的思(tao)路(是什么-为何-怎么作)html

是什么

Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class.vue

简单来讲,上面这段官腔大概翻(xia)译(shuo)就是告诉咱们class可以作到的老子用hooks基本能够作到,放弃抵抗吧,少年!react

其实按照我本身的见解:React Hooks是在函数式组件中的一类以use为开头命名的函数。 这类函数在React内部会被特殊对待,因此也称为钩子函数。编程

  • 函数式组件

Hooks只能用于Function Component, 其实这么说不严谨,我更喜欢的说法是建议只在于Function Component使用Hooksredux

Should I use Hooks, classes, or a mix of both?
excuse me?

  • use开头

React 约定,钩子一概使用use前缀命名,便于识别,这没什么可说的,要被特殊对待,就要服从必定的规则api

  • 特殊对待

Hooks做为钩子,存在与每一个组件相关联的“存储器单元”的内部列表。 它们只是咱们能够放置一些数据的JavaScript对象。 当你像使用useState()同样调用Hook时,它会读取当前单元格(或在第一次渲染时初始化它),而后将指针移动到下一个单元格。 这是多个useState()调用每一个get独立本地状态的方式数组

为何

解决为何要使用hooks的问题,我决定从hooks解决了class组件的哪些痛点和hooks更符合react的组件模型两个方面讲述。性能优化

1. class组件不香吗?

class组件它香,可是暴露的问题也很多。Redux 的做者 Dan Abramov总结了几个痛点:bash

  • Huge components that are hard to refactor and test.
  • Duplicated logic between different components and lifecycle methods.
  • Complex patterns like render props and higher-order components.

第一点:难以重构和测试的巨大组件。 若是让你在一个代码行数300+的组件里加一个新功能,你不慌吗?你尝试过注释一行代码,结果就跑不了或者逻辑错乱吗?若是须要引入redux或者定时器等那就更慌了~~网络

第二点:不一样组件和生命周期方法之间的逻辑重复。 这个难度不亚于蜀道难——难于上青天!固然对于简单的逻辑可能经过HOCrender props来解决。可是这两种解决办法有两个比较致命的缺点,就是模式复杂和嵌套。

第三点:复杂的模式,好比render props和 HOC。 不得不说我在学习render props的时候不由发问只有在render属性传入函数才是render props吗?好像我再任意属性(如children)传入函数也能实现同样的效果; 一开始使用HOC的时候打开React Develops Tools一看,Unknown是什么玩意~看着一层层的嵌套,我也是无能为力。

以上这三点均可以经过Hooks来解决(疯狂吹捧~)

2. hooks更符合React的编程模型

咱们知道,react强调单向数据流和数据驱动视图,说白了就是组件和自上而下的数据流能够帮助咱们将UI分割,像搭积木同样实现页面UI。这里更增强调组合而不是嵌套,class并不能很完美地诠释这个模型,可是hooks配合函数式组件却能够!函数式组件的纯UI性配合Hooks提供的状态和反作用能够将组件隔离成逻辑可复用的独立单元,逻辑分明的积木他不香吗!

真香

怎么作

别问,问就是文档,若是不行的话,请熟读并背诵文档...

可是(万事万物最怕But), 既然是实践,就得伪装实践过,下面就说说本人的简单实践和想法吧。

1. 转变心智模型

jQuery
我认为学习Hooks的主要成本不在于api的学习,而是在于 心智模型的转变~就像是当年react刚出时,jQuery盛行的时代,这也须要时间去理解这种基于virtual DOM的心智模型。出于本能,咱们总喜欢在新事物的身上寻找旧事物的共同点,这种惯性思惟应该批判性地对待(上升到哲学层面了,赶忙回归正题....),若是你在学习的过程当中也有过把class组件的那套搬到Hooks,那么恭喜你,你可能会陷入无限的wtf····, 下面举几个例子

  1. state一把梭
// in class component
class Demo extends React.Component {
 constructor(props) {
   super(props)
   this.state = {
     name: 'Hello',
     age: '18',
     rest: {},
   }
 }
 ...
}

// in function component
function Demo(props) {
 const initialState = {
   name: 'Hello',
   age: '18',
   rest: {},
 }
 const [state, setState] = React.useState(initialState)
 ...
}
复制代码
  1. 尝试模拟生命周期
// 这么实现很粗糙,能够配合useRef和useCallback,但即便这样也不彻底等价于componentDidMount
function useDidMount(handler){
  React.useEffect(()=>{
      handler && handler()
  }, [])
}
复制代码
  1. 在useEffect使用setInterval有时会事与愿违
// count更新到1就不动了
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  ...
}
复制代码

其实,在class component环境下思考问题更像是在特定的时间点作特定的事情,例如咱们会在constructor中初始化state,会在组件挂载后(DidMount)请求数据等,会在组件更新后(DidUpdate)处理状态变化的逻辑,会在组件卸载前(willUnmount)清除一些反作用

然而在hooks+function component环境下思考问题应该更趋向于特定的功能逻辑,以功能为一个单元去思考问题会有一种豁然开朗的感受。例如改变document的title、网络请求、定时器... 对于hooks,只是为了实现特定功能的工具而已

你会发现大部分你想实现的特定功能都是有反作用(effect)的,能够负责任的说useEffect是最干扰你心智模型的Hooks, 他的心智模型更接近于实现状态同步,而不是响应生命周期事件。还有一个可能会影响你的就是每一次渲染都有它本身的资源,具体表现为如下几点

  • 每一次渲染都有它本身的Props 和 State:当咱们更新状态的时候,React会从新渲染组件。每一次渲染都能拿到独立的状态值,这个状态值是函数中的一个常量(也就是会说,在任意一次渲染中,props和state是始终保持不变的)
  • 每一次渲染都有它本身的事件处理函数:和props和state同样,它们都属于一次特定的渲染,即使是异步处理函数也只能拿到那一次特定渲染的状态值
  • 每个组件内的函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获某次渲染中定义的props和state(建议在分析问题时,将每次的渲染的props和state都常量化)

2. 所谓Hooks实践

useState —— 相关的状态放一块儿

  • 不要全部state一把梭,能够写多个useState,基本原则是相关的状态放一块儿
  • setXX的时候建议使用回调的形式setXXX(xxx => xxx...)
  • 管理复杂的状态能够考虑使用useReducer(如状态更新依赖于另外一个状态的值)
// 实现计数功能
 const [count, setCount] = React.useState(0);
 setCount(count => count + 1)
 
// 展现用户信息
const initialUser = {
  name: 'Hello',
  age: '18',
}
const [user, setUser] = React.useState(initialUser)
复制代码

useEffect —— 不接受欺骗的反作用

  • 不要对依赖数组撒谎,effect中用到的全部组件内的值都要包含在依赖中。这包括props,state,函数等组件内的任何东西
  • 不要滥用依赖数组项, 让Effect自给自足
  • 经过返回一个函数来清除反作用,在从新渲染后才会清除上一次的effects
// 修改上面count更新到1就不动了,方法1
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, [count]);
  ...
}
// 修改上面count更新到1就不动了,方法2( 与方法1的区别在哪里 )
function Counter() {
  const [count, setCount] = React.useState(0);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count => count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  ...
}
复制代码

关于useEffect, 墙裂推荐Dan Abramov的A Complete Guide to useEffect,一篇支称整篇文章架构的深度好文!

useReducer —— 强大的状态管理机制

  • 把组件内发生了什么(actions)和状态如何响应并更新分开表述,是Hooks的做弊模式
/** 修改需求:每秒不是加多少能够由用户决定,能够看做不是+1,而是+step*/

// 方法1
function Counter() {
  const [count, setCount] = React.useState(0);
  const [step, setStep] = React.useState(1);
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count => count + step);
    }, 1000);
    return () => clearInterval(id);
  }, [step]);
  ...
}
// 方法2( 与方法1的区别在哪里 )
const initialState = {
  count: 0,
  step: 1,
};

function reducer(state, action) {
  const { count, step } = state;
  if (action.type === 'tick') {
    return { ...state, count: count + step };
  } else if (action.type === 'step') {
    return { ...state, step: action.step };
  }
}

function Counter() {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  const { count, step } = state;

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);
  ...
}
复制代码

useCallback —— FP里使用函数的好搭档

说这个以前,先说一说若是你要在FP里面使用函数,你要先要思考有替代方案吗?

方案1: 若是这个函数没有使用组件内的任何值,把它提到组件外面去定义

方案2:若是这个函数只是在某个effect里面用到,把它定义到effect里面

若是没有替代方案,就是useCallback出场的时候了。

  • 返回一个 memoized 回调, 不要对依赖数组撒谎
// 场景1:依赖组件的query
function Search() {
  const [query, setQuery] = React.useState('hello');
  
  const getFetchUrl = React.useCallback(() => {
    return `xxxx?query=${query}`;
  }, [query]);  

  useEffect(() => {
    const url = getFetchUrl();
  }, [getFetchUrl]); 
  ...
}

// 场景2:做为props
function Search() {
   const [query, setQuery] = React.useState('hello');

  const getFetchUrl = React.useCallback(() => {
    return `xxxx?query=${query}`;
  }, [query]);  

  return <MySearch getFetchUrl={getFetchUrl} />
}

function MySearch({ getFetchUrl }) {
  useEffect(() => {
    const url = getFetchUrl();
  }, [getFetchUrl]); 
  ...
}
复制代码

useRef —— 有记忆功能的可变容器

  • 返回一个可变的 ref 容器对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变,也就是说会在每次渲染时返回同一个 ref 对象
  • 当 ref 对象内容发生变化时,useRef 并不会通知你。变动 .current 属性不会引起组件从新渲染
  • 能够在ref.current 属性中保存一个可变值的“盒子“。常见使用场景:存储指向真实DOM / 存储事件监听的句柄 / 记录Function Component在某次渲染的值( eg:上一次state/props,定时器id.... )
// 存储不变的引用类型
const { current: stableArray } = React.useRef( [1, 2, 3] )
<Comp arr={stableArray} />

// 存储dom引用
const inputEl = useRef(null);
<input ref={inputEl} type="text" />

// 存储函数回调
const savedCallback = useRef();
useEffect(() => {
    savedCallback.current = callback;
}
复制代码

useMemo —— 记录开销大的值

// 此栗子来自文档
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码

useContext —— 功能强大的上下文

  • 接收一个 context (React.createContext 的返回值)并返回该 context 的当前值,当前的 context 值由上层组件中最早渲染的 <MyContext.Provider value={value}> 的 value决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值,若是从新呈现组件很是昂贵,那么能够经过使用useMemo来优化它
// 此栗子来自文档
const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}
复制代码

彩蛋

说是彩蛋,实际上是补充说明~~

1. 一条重要的规则(代码不规范,亲人两行泪)

hooks除了要以use开头,还有一条很很很很重要的规则,就是hooks只容许在react函数的顶层被调用(这里墙裂推荐Hooks必备神器eslint-plugin-react-hooks)

考虑到出于研(gang)究(jing)精神的你可能会问,为何不能这么用,我偏要的话呢?若是我是hooks开发者,我会坚决果断地说出门右转,有请下一位开发者!固然若是你想知道为何这么约定地话,仍是值得探讨一下的。其实这个规则就是保证了组件内的全部hooks能够按照顺序被调用。那么为何顺序这么重要呢,不能够给每个hooks加一个惟一的标识,这样不就能够随心所欲了吗?我之前一直都这么想过直到Dan给了我答案,简单点说就是为了hooks最大的闪光点——custom-hooks

2. custom-hooks

给个人感受就是custom-hooks是一个真正诠释了React的编程模型的组合的魅力。你能够不看好它,但它确实有过人之处,至少它呈现出思想让我越想越上头~~以致于vue3.0也借鉴了他的经验,推出了Vue Hooks。反手推荐一下react conf 2018的custom-hooks。

魅力

// 修改页面标题
function useDocumentTitle(title) {
  useEffect (() => {
    document.title = title;
  }, [title]);
}

// 使用表单的input
function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  return {
    value,
    onChange: handleChange
  };
}
复制代码

写在最后

最后抛出两个讨论的小问题。

  1. React Hooks没有缺点吗?

    • 确定是有的,给我最直观的感觉就是使人又爱又恨的闭包
    • 不断地重复渲染会带来必定的性能问题,须要人为的优化
  2. 上面说了写了不少的setInterval的代码,能够考虑封装成一个custom-hooks?

    • 能够考虑封装成useInterva,关于封装仍是墙裂推荐Dan的 Making setInterval Declarative with React Hooks
    • 若是有一堆特定的功能hooks,是否是彻底能够经过组装各类hooks完成业务逻辑的开发,例如网络请求、绑定事件监听等

本人能力有限,若是有哪里说得不对的地方,欢迎批评指正!

对不听系列

真的真的最后,怕你错过,再次安利Dan Abramov的A Complete Guide to useEffect,一篇支称整篇文章架构的深度好文!