10 分钟快速入门:React Hooks

865 阅读7分钟

在开始之前,先看一张图:

hooks vs class-代码组织复杂度对比

为什么要推出 React Hooks?

React Hooks 的设计目的,就是加强版函数组件,完全不使用"类",就能写出一个全功能的组件。

不准确的总结一下,就是:React 团队希望开发者们少用类组件,多用函数组件。

这里我们就有一个疑问了:类组件有啥不好?函数组件有啥好?

类组件的缺点

笔者认为,组件类有这么几个问题:

  • 积重难返:大型组件很难拆分和重构,也很难测试。
  • 逻辑分散:业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  • 难以复用:为了在类组件的基础上实现复用,引入了复杂的编程模式,比如 render props 和高阶组件。

React 团队希望:

组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可。

组件的最佳写法应该是函数,而不是类。

从实现一个组件内请求说起……

下面,我将尝试通过实现一个组件内请求,来尝试说明类组件和函数组件的不同。

在类组件中,我们如果要实现发请求,得这么做:

import React from 'react';

class Component extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false,
      info: null,
    };
  }

  componentDidMount() {
    this.fetchInfo();
  }

  fetchInfo = async () => {
    this.setState({ loading: true });
    const info = await getInfo();
    this.setState({
      info,
      loading: false,
    });
  }

  render() {
    const { info } = this.state;
    return <div>{info}</div>;
  }
}

export default Component;

问题1:逻辑分散

简单的一个组件内请求,业务逻辑分散在了组件的 4 个方法里面。

  • constructor
    • state 初始化
  • componentDidMount
    • 执行请求方法
  • fetchInfo
    • 发起请求,处理数据
  • render
    • 根据状态返回内容

当组件逐渐搭起来之后,开发者一旦疏忽,就很容易导致重复逻辑或关联逻辑。

问题2:难以拆分和重构,也很难测试

在上述代码中,请求的逻辑是跟组件的生命周期强耦合的,代码放在了 3 个 react 生命周期钩子函数中。

当组件逐渐大起来之后,一个 componentDidMount 可能都数十甚至上百行,想要解耦、拆分、重构,谈何容易呀!

问题3:引入新功能麻烦,对开发者不够简单

此时,我们想要加入一个 loading 状态,那么必须:

  • 在 constructor 里,this.state 中声明一个 loading
  • 在 fetchInfo 中加入对 loading 的状态处理
  • 在 render 中对 loading 做特殊判断

那么,我如果还想做「解析参数、依赖处理、全局配置、请求数据逻辑、回调处理、循环请求、缓存处理」等功能呢?

靓仔语塞。

问题4:难以复用

上面这些代码都是与组件的生命周期强相关的,难以将其抽象出来。为了实现抽象的目的,我们只能借助一些复杂的编程模式,如渲染属性(render props)和高阶组件(HOC)。

那如果用 react hooks 要怎么操作?

简单版:

import { useState, useEffect } from 'react';

const Component = () => {
  const [ loading, setLoading ] = useState(false);
  const [ info, setInfo ] = useState('');

  useEffect(() => {
    setLoading(true);
    const info = await getInfo();
    setLoading(false);
    setInfo(info);
  }, []);

  if (loading) return <div>加载中……</div>;
  return <div>{info}</div>
}

export default Component;

但是,别忘了,Hooks 最重要的能力就是逻辑复用!这些逻辑我们完全可以封装起来!

进阶版:

import { useState, useEffect } from 'react';

function useInfo() {
  const [ loading, setLoading ] = useState(false);
  const [ info, setInfo ] = useState('');

  useEffect(() => {
    setLoading(true);
    const info = await getInfo();
    setLoading(false);
    setInfo(info);
  }, []);
  return { loading, info };
}

const Component = () => {
  const { info, loading } = useInfo();

  if (loading) return <div>加载中……</div>;
  return <div>{info}</div>
}

export default Component;

我的天,请求方法居然被抽象出来了 !它(useInfo)可以当做一个通用逻辑被复用了!

所有请求相关的处理逻辑,都放在了 userInfo 这里。

它的好处一目了然:

  • 学习成本低,一眼就知道你这个代码想干嘛
  • 业务逻辑集中,所有东西都在 useInfo 里面
  • 可以复用,直接把 useInfo 拿出去,就能到处跑

代码组织复杂度对比:类组件 VS 函数组件

hooks vs class-代码组织复杂度对比

React Hooks 有哪几个 API?分别都是干什么用的?

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩”进来。

函数组件的一些特性

在了解 API 之前,我想先跟你说说函数组件的一些特性,方便你理解:

  • 每次 state、props 改变,都会重新执行一遍;
  • 函数组件中的 useXXX 只会创建一次;
  • 函数跑完之后,返回了新的 jsx 之后,才会执行 useEffect。useEffect 相当于 componentDidXXX。

当你不能理解下面的 API 时,回过头来看看这函数组件的这几个特性,能帮助你更好的理解它们。

useState():状态钩子

useState()用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

import React, { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

上面代码等同于:

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>
    );
  }
}

useContext():共享状态钩子

如果需要在组件之间共享状态,可以使用useContext()。

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>
  );
}

useReducer():action 钩子

useReducers() 钩子用来引入类似 Redux 中的 Reducer 功能(不完全版)。

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 (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useEffect():副作用钩子

useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。以前,放在componentDidMount里面的代码,现在可以放在useEffect()。

import React, { useState, useEffect } from 'react';

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

  // Similar to componentDidMount and componentDidUpdate:
  useEffect(() => {
    // Update the document title using the browser API
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

上面的代码等同于:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

React Hooks 为什么这么香?

在我看来,React Hooks 必然是 React 开发的大势所趋,原因就在于:

React Hooks 提供了开发者对一个功能做精做细的能力。大量出现优秀的功能库,使用者们只需要调用一句 useXXX,就可以解决一个大问题。

举个栗子:

在社区里面,有一个非常棒的基于 react hooks 开发的请求库:SWR。在这个库里面,他解决了请求方方面面的问题:解析参数、依赖处理、全局配置、请求数据逻辑、回调处理、循环请求、缓存处理……

想象一下,这样的场景,在如果要在类组件里面实现,而且希望你可以复用,实现起来该多难!

但 react hooks 出现之后,一扫阴霾。我们只需要基于 react hooks 的各个 API,实现了所需功能之后,将它都封装在一个 useSWR 里面。

对于开发者,只需要一句话:const { data, error } = useSWR('/api/user', fetcher) 就能用上所有功能了!简直不要太方便啊!逻辑集中且明确,一次编写多处复用,香!

另外,社区中还有很多优秀的库,这么一些合集:

Collection of React Hooks

awesome-react-hooks

@umijs/hooks

react-use

react-query - Hooks for fetching

他们能大大的提高开发者的效率,非常值得大家去了解和使用。