先说明一下,React Hooks,目前,还处于,RFC(请求评论)阶段,尚未,正式发布。本文,是,基于,目前,已经,公开的,RFC文档,和,社区讨论,对,React Hooks,进行的,详细解读。正式发布时,API,可能,会有,一些,调整。

虽然,Hooks,还,未正式发布,但是,从,已经,公开的,信息来看,Hooks,将会,彻底,改变,我们,编写,React组件的,方式。它,让,函数组件,也,能拥有,状态,和,生命周期,不再,需要,类组件。

今天,我就,基于,目前,公开的,RFC,和,讨论,对,React Hooks,进行,详细的,解读,从基础,到高级。希望,能帮助大家,提前,了解,这个,重要的,新特性,更好地,准备,迎接,React的未来。

一、为什么,需要,Hooks?

在,介绍,Hooks,之前,先,说说,为什么,React团队,要,开发,Hooks。

在,Hooks,之前,React组件,有,两种,写法:类组件,和,函数组件。

类组件,功能,强大,可以,拥有,状态(state),和,生命周期方法(componentDidMount,componentDidUpdate,等)。但是,类组件,也,有,一些,问题:

  1. this指向问题:类组件中,经常,会,遇到,this指向的,问题。比如,事件处理器中,this,不是,组件实例。需要,用,bind,或者,箭头函数,来,解决。
  2. 生命周期方法,逻辑分散:相关的,逻辑,经常,分散在,不同的,生命周期方法中。比如,订阅,在,componentDidMount中,取消订阅,在,componentWillUnmount中。更新逻辑,在,componentDidUpdate中。这,使得,代码,难以,理解,和,维护。
  3. 逻辑复用困难:在,类组件中,复用,状态逻辑,比较困难。通常,需要,用,高阶组件(HOC),或者,render props,来,实现。但是,这,两种,方式,都会,导致,组件树,嵌套,过深,出现,"嵌套地狱"。
  4. 类组件,难以,优化:类组件,对于,编译器,的,优化,不太,友好。比如,组件,预编译,内联,等,优化,在,类组件中,比较,难,实现。

函数组件,简洁,优雅,没有,this指向问题,也,容易,优化。但是,函数组件,没有,状态,和,生命周期,只能,用于,简单的,展示组件。

为了,让,函数组件,也,能,拥有,状态,和,生命周期,同时,解决,类组件的,上述,问题,React团队,提出了,Hooks。

Hooks,是,一些,特殊的,函数,它们,可以,让你,在,函数组件中,"钩入",React的,状态,和,生命周期,等,特性。

有了,Hooks,你,就,可以,完全,用,函数组件,来,编写,所有的,React组件,不再,需要,类组件。

二、Hooks的,基本规则

在,介绍,具体的,Hooks,之前,先,说说,Hooks的,两个,基本规则:

1. 只在,最顶层,调用,Hooks

不要,在,循环,条件,嵌套函数,中,调用,Hooks。只在,React函数组件的,最顶层,调用,Hooks。

这是因为,React,是,根据,Hooks,被,调用的,顺序,来,关联,状态,和,Hooks的。如果,在,循环,条件,中,调用,Hooks,可能,会,导致,调用顺序,变化,从而,导致,bug。

2. 只在,React函数组件,中,调用,Hooks

不要,在,普通的,JavaScript函数,中,调用,Hooks。只在,React函数组件,或者,自定义Hooks,中,调用,Hooks。

遵循,这,两个,规则,就能,正确地,使用,Hooks。React团队,还,提供了,一个,ESLint插件,来,自动,检查,是否,遵循了,这,两个,规则。

三、useState:状态钩子

useState,是,最基础的,一个,Hook。它,让,函数组件,也,能,拥有,状态。

useState,接收,一个,初始值,返回,一个,数组。数组的,第一个,元素,是,当前的,状态值。第二个,元素,是,一个,更新状态的,函数。

import { useState } from 'react';

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

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        点击我
      </button>
    </div>
  );
}

上面的,代码,用,useState,创建了,一个,名为,count的,状态,初始值,是,0。setCount,是,更新,count的,函数。点击按钮,调用,setCount(count + 1),就,会,把,count,加1,并且,重新,渲染,组件。

和,类组件的,setState,不同,useState的,更新函数,不会,自动,合并,状态。如果你,有,多个,状态,需要,分别,调用,useState。

function Example() {
  const [name, setName] = useState('张三');
  const [age, setAge] = useState(20);
  const [address, setAddress] = useState('北京');
  // ...
}

当然,你,也,可以,把,相关的,状态,放在,一个,对象里,用,一个,useState。但是,更新的时候,需要,手动,合并。

function Example() {
  const [user, setUser] = useState({
    name: '张三',
    age: 20,
    address: '北京',
  });

  const updateName = (newName) => {
    setUser(prev => ({
      ...prev,
      name: newName,
    }));
  };
  // ...
}

useState的,更新函数,还,支持,函数式更新。如果,新的,状态,依赖于,之前的,状态,推荐,用,函数式更新。

setCount(prev => prev + 1);

这,和,类组件的,setState的,函数式更新,类似。

四、useEffect:副作用钩子

useEffect,是,另一个,非常,重要的,Hook。它,让,函数组件,也,能,拥有,副作用。

副作用,指的是,数据获取,订阅,定时器,手动,修改DOM,等,操作。在,类组件中,这些,操作,通常,放在,componentDidMount,componentDidUpdate,componentWillUnmount,等,生命周期方法中。

useEffect,把,这些,生命周期方法,统一了起来。

useEffect,接收,两个,参数。第一个,参数,是,一个,函数,就是,副作用,的,逻辑。第二个,参数,是,一个,依赖数组,可选。

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    document.title = `你点击了 ${count} 次`;
  });

  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>
        点击我
      </button>
    </div>
  );
}

上面的,代码,用,useEffect,在,组件,渲染后,更新,document.title。每次,组件,重新渲染,useEffect,都会,执行。

这,相当于,类组件的,componentDidMount,和,componentDidUpdate,的,组合。

1. 清理副作用

有些,副作用,需要,清理。比如,订阅,定时器,等。在,类组件中,通常,在,componentDidMount中,订阅,在,componentWillUnmount中,取消订阅。

在,useEffect中,你,可以,在,副作用函数中,返回,一个,清理函数。这个,清理函数,会,在,组件,卸载时,或者,下一次,useEffect,执行前,调用。

useEffect(() => {
  const timer = setInterval(() => {
    console.log('tick');
  }, 1000);

  return () => {
    clearInterval(timer);
  };
});

上面的,代码,创建了,一个,定时器。返回的,清理函数,会,清除,这个,定时器。

这,相当于,类组件的,componentDidMount,和,componentWillUnmount,的,组合。

2. 依赖数组

默认情况下,useEffect,每次,组件,渲染后,都会,执行。但是,有时候,我们,只希望,在,某些,值,变化时,才,执行,useEffect。

这,就,需要,用到,useEffect的,第二个,参数,依赖数组。

useEffect(() => {
  document.title = `你点击了 ${count} 次`;
}, [count]);

上面的,代码,只有,在,count,变化时,才,会,执行,useEffect。如果,count,没有,变化,useEffect,就,不会,执行。

如果,依赖数组,是空数组,[],那么,useEffect,只,会,在,组件,挂载时,执行,一次。这,相当于,类组件的,componentDidMount。

useEffect(() => {
  // 只在挂载时执行一次
  fetchData();
}, []);

依赖数组,是,useEffect,中,非常,重要的,一个,概念。正确地,使用,依赖数组,可以,避免,不必要的,副作用执行,提升,性能。

但是,也要,注意,依赖数组,中,的值,一定要,正确。如果,遗漏了,某个,依赖,可能,会,导致,bug。React团队,推荐,用,ESLint插件,来,检查,依赖数组,是否,正确。

五、useContext:上下文钩子

useContext,让,函数组件,也,能,方便地,使用,React Context。

在,Hooks,之前,使用,Context,通常,需要,用,Consumer组件,或者,static contextType。这,两种,方式,都,比较,繁琐。

useContext,让,使用,Context,变得,非常,简单。

useContext,接收,一个,Context对象,返回,当前的,Context值。

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

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

function Toolbar() {
  const theme = useContext(ThemeContext);
  return (
    <div>
      当前主题:{theme}
    </div>
  );
}

上面的,代码,用,createContext,创建了,一个,ThemeContext。在,App组件中,用,Provider,提供了,Context值,"dark"。在,Toolbar组件中,用,useContext,获取了,当前的,Context值。

当,Context值,变化时,使用,useContext的,组件,会,自动,重新渲染。

useContext,让,跨层级的,数据传递,变得,非常,简单。不再,需要,一层一层地,传递props。

六、useReducer:状态管理钩子

useReducer,是,另一个,用于,状态管理的,Hook。它,类似于,Redux的,reducer。

如果,你的,状态逻辑,比较复杂,包含,多个,子值,或者,下一个,状态,依赖于,之前的,状态,那么,useReducer,可能,比,useState,更,合适。

useReducer,接收,三个,参数:

  • reducer:一个,函数,接收,当前的,state,和,action,返回,新的,state。
  • initialState:初始状态。
  • init:可选,一个,函数,用于,惰性,初始化,状态。

useReducer,返回,一个,数组。第一个,元素,是,当前的,state。第二个,元素,是,dispatch函数,用于,发送,action。

import { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>计数:{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

上面的,代码,用,useReducer,实现了,一个,计数器。reducer函数,根据,action的,type,来,更新,状态。dispatch函数,用于,发送,action。

useReducer,和,useState,类似,但是,更,适合,复杂的,状态逻辑。而且,useReducer,还,方便,测试。因为,reducer,是,一个,纯函数,可以,独立,测试。

另外,useReducer,和,useContext,结合,可以,实现,简单的,状态管理,替代,Redux,在,一些,中小规模的,应用中。

七、useMemo:记忆化钩子

useMemo,用于,记忆化,计算结果。它,会,记住,上一次的,计算结果,只有,当,依赖,变化时,才,重新,计算。

这,对于,昂贵的,计算,非常,有用,可以,避免,不必要的,重复计算,提升,性能。

useMemo,接收,两个,参数:

  • factory:一个,函数,返回,要,记忆化的,值。
  • deps:依赖数组。

useMemo,返回,记忆化的,值。

import { useMemo } from 'react';

function Example({ items, filter }) {
  const filteredItems = useMemo(() => {
    return items.filter(item => item.name.includes(filter));
  }, [items, filter]);

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

上面的,代码,用,useMemo,记忆化了,过滤后的,items。只有,当,items,或者,filter,变化时,才,重新,过滤。否则,直接,返回,上一次的,结果。

这,在,items,数量,很大,过滤,很耗时的,情况下,能,显著,提升,性能。

需要,注意的是,useMemo,只是,一种,性能优化的,手段。不要,过度,依赖,它。React,可能,会,在,某些,情况下,丢弃,记忆化的,值,重新,计算。所以,你的,代码,应该,在,没有,useMemo的,情况下,也,能,正确,运行。

八、useRef:引用钩子

useRef,用于,创建,一个,可变的,引用对象。这个,引用对象,的,current属性,指向,你,传入的,初始值。

useRef,和,useState,类似,但是,有,一个,重要的,区别:修改,ref的,current属性,不会,触发,组件,重新渲染。

useRef,有,两个,主要的,用途:

1. 访问DOM元素

这,是,useRef,最常见的,用途。你,可以,用,useRef,创建,一个,ref,然后,把它,附加到,一个,DOM元素上。这样,你,就,可以,通过,ref.current,访问,这个,DOM元素。

import { useRef, useEffect } from 'react';

function Example() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return (
    <input ref={inputRef} type="text" />
  );
}

上面的,代码,用,useRef,创建了,一个,inputRef,附加到,input元素上。在,组件,挂载后,调用,inputRef.current.focus(),让,input,自动,获取焦点。

这,相当于,类组件的,React.createRef。

2. 保存,可变值

useRef,还,可以,用于,保存,任何,可变值。因为,修改,ref的,current属性,不会,触发,重新渲染,所以,它,适合,保存,一些,不需要,触发,渲染的,值。

比如,定时器ID,上一次的,props,等。

import { useRef, useEffect } from 'react';

function Example() {
  const timerRef = useRef(null);

  useEffect(() => {
    timerRef.current = setInterval(() => {
      console.log('tick');
    }, 1000);

    return () => {
      clearInterval(timerRef.current);
    };
  }, []);

  // ...
}

上面的,代码,用,useRef,保存了,定时器ID。在,清理函数中,清除,定时器。

九、自定义Hooks

除了,React,内置的,Hooks,你,还,可以,创建,自己的,自定义Hooks。

自定义Hooks,就是,一个,普通的,JavaScript函数,但是,它的,名字,以,"use",开头,并且,它,调用了,其他的,Hooks。

自定义Hooks,让,你,可以,把,组件,逻辑,提取出来,复用。

比如,我们,可以,创建,一个,useWindowSize的,自定义Hook,用于,获取,窗口的,大小。

import { useState, useEffect } from 'react';

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);

    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return size;
}

然后,在,组件中,使用,这个,自定义Hook。

function MyComponent() {
  const size = useWindowSize();

  return (
    <div>
      窗口宽度:{size.width},窗口高度:{size.height}
    </div>
  );
}

自定义Hooks,是,Hooks,中,最,强大的,部分之一。它,让,状态逻辑的,复用,变得,非常,简单。不再,需要,高阶组件,或者,render props,也就,不会,出现,"嵌套地狱"。

你,可以,把,任何,可复用的,组件逻辑,提取成,自定义Hooks。比如,数据获取,表单处理,订阅,定时器,等。

十、Hooks的,注意事项,和,最佳实践

最后,说说,Hooks的,一些,注意事项,和,最佳实践。

1. 遵循,Hooks的,基本规则

只在,最顶层,调用,Hooks。只在,React函数组件,或者,自定义Hooks,中,调用,Hooks。

推荐,使用,React团队,提供的,ESLint插件,来,自动,检查。

2. 正确,使用,依赖数组

useEffect,useMemo,useCallback,等,Hooks,都,有,依赖数组。一定要,正确地,填写,依赖数组。不要,遗漏,依赖,也,不要,添加,不必要的,依赖。

如果,遗漏了,依赖,可能,会,导致,闭包,捕获了,旧的值,从而,导致,bug。

3. 不要,过度,使用,useMemo,和,useCallback

useMemo,和,useCallback,是,性能优化的,手段。但是,它们,本身,也,有,开销。不要,在,所有的,地方,都,用,useMemo,和,useCallback。只,在,确实,有,性能问题的,地方,使用。

而且,React,未来,可能,会,自动,进行,一些,记忆化,优化。到时候,手动的,useMemo,和,useCallback,可能,就,不需要了。

4. 自定义Hooks,命名,以,"use",开头

自定义Hooks的,名字,一定要,以,"use",开头。这样,React的,ESLint插件,才能,识别,它,是,一个,Hook,并且,检查,它,是否,遵循了,Hooks的,规则。

5. 渐进式,采用,Hooks

Hooks,和,类组件,可以,共存。你,不需要,一下子,把,所有的,类组件,都,改成,函数组件。你,可以,在,新的,组件中,使用,Hooks。然后,逐步地,把,旧的,类组件,重构,成,函数组件。

React团队,也,承诺,会,继续,支持,类组件。所以,你,不用担心,类组件,会,被,废弃。

十一、写在最后

以上,就是,基于,目前,公开的,RFC,和,讨论,对,React Hooks,的,详细解读。

虽然,Hooks,还,未正式发布,API,可能,还,会,有,一些,调整。但是,整体的,设计,和,思想,应该,不会,有,太大的,变化。

Hooks,是,React,自,诞生以来,最重要的,一次,变革。它,彻底,改变了,我们,编写,React组件的,方式。它,让,函数组件,拥有了,类组件的,所有,能力,同时,解决了,类组件的,很多,问题。

我,相信,在,不久的,将来,Hooks,会,成为,React组件的,主流,写法。类组件,会,逐渐,被,函数组件,取代。

所以,提前,了解,和,学习,Hooks,是,非常,有必要的。希望,这篇文章,能,帮助大家,更好地,理解,Hooks,更好地,准备,迎接,React的未来。

当然,因为,Hooks,还,未正式发布,本文,中的,一些,内容,可能,会,和,正式版,有,出入。如果,有,错误,或者,不准确的,地方,欢迎,大家,在,评论区,指正。

也,欢迎,大家,在,评论区,分享,你,对,React Hooks,的,看法,和,期待。