10 分钟快速入门:React Hooks

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


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 里面

  • 10 分钟快速入门:React Hooks

    为什么要推出 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

他们能大大的提高开发者的效率,非常值得大家去了解和使用。可以复用,直接把 useInfo 拿出去,就能到处跑

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

[图片上传失败...(image-540c5f-1582016358590)]

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260