9

React Hooks 快速入门与开发体验(一)

 3 years ago
source link: http://blog.krimeshu.com/2021/01/17/some-thinking-about-react-hooks-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Vue 3 推出 Composition API 的时候,看到一些表示这和 React Hooks 很像的评论。

正好最近有个项目改用了 React 的,于是趁机体验了一下 React Hooks,看看是否真是如此。

简介

说来惭愧,上次使用 React,还是几年前想在 React 项目里想要实现组件样式作用域,对比和选择 css-modulesstyled-components 方案来着,最终实现体验还是不怎么样,后来大部分项目都是小程序、Vue 和 node.js 印象就还停留在那个年代。

那什么是 React Hook 呢?官方的介绍如下:

Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

(来源链接: https://zh-hans.reactjs.org/docs/hooks-custom.html

其中的 class 指的应该是 ES Class 也就是类语法,而 state 应该就是指平时通过 setState 更新状态来触发重渲染的组件 state 属性了。

并且官方保证它 没有破坏性改动

React Hook 是:

  • 完全可选 的,可以轻松引入。如果你不喜欢,也可以不去学习和使用。
  • 100% 向后兼容 ,React Hook 不会包含任何破坏性改动。
  • 现在可用 ,Hook 已发布于 v16.8.0。

第一条说明官方并不强制要求使用 React Hook。第二条则是说明,使用它不会影响旧版代码,确保存量项目代码的正常工作。

至于支持 Hook 的 React 版本,大约发布于2018年底。到本文的2021年初算来,差不多已经过去两年时间了。

不过需要注意 React Hook 的使用规则:

  • 只能在 函数最外层 调用 Hook。
  • 只能在 React 的函数组件 中调用 Hook。

第二条很好理解,毕竟是为函数组件所设计的,第一条究竟为何,没有实际体验也很难说清楚,我们容后再叙。

既然已经出来两年之久,这个 React Hook 实际使用起来究竟效果如何呢?

一、基础 Hooks

1. useState

基础说明

比如一个简单的点击计数示例,其中使用到一个计数 state,在每次点击后将其 +1 后更新视图:

import React, { Component } from 'react';

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

    render () {
        const { count } = this.state;
        const setCount = (count) => this.setState({ count });

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

如果使用 React Hooks 实现,就只需要这样:

import React, { useState } from 'react';

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

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

这个例子中可以看出,React Hook 相比组件类:

  • 将组件从带有 多个生命周期函数的类声明 ,直接简化为 一个渲染函数的函数组件
  • 组件渲染时用到的属性和对应更新回调,通过一个名为 useState 的 Hook 来实现。
  • 对于组件类的生命周期函数,应该也可以通过其它 Hook 实现。

其它生命周期函数我们稍后再叙,先来看看一些上面的例子没有提到的情况:

获取组件的 props

对于组件 props 的获取很简单,函数组件的第一个传入参数就是了:

function Child({ name }) {
    return (
        <p>Name: {name}</p>
    );
}

function Parent() {
    const children = [
        { id: 1, name: 'Adam' },
        { id: 2, name: 'Bernard' },
    ];

    return (
        <div>
            {children.map(({ id, name }) => (
                <Child key={id} name={name} />
            ))}
        </div>
    );
}

更新数组/对象类型的 state

对于简单的值类型 state,直接使用 useState 返回的更新函数就可以轻松完成更新了。

对于数组和键值对(对象)类型的数据,又该怎么更新呢?

难道直接把 整个新的数组/对象传入更新函数

——没错。

不过这样操作可能会稍显繁琐,因为 必须传入一个新的数组/对象才能触发更新 。直接修改原对象后直接传入更新函数的话,并不会触发重渲染。

所以我们需要创建一个数组/对象的拷贝,再传给更新函数,通常可以使用ES6数组方法和解构赋值对操作稍作简化:

function Example() {
    const [list, setList] = useState([
        'hello world',
    ]);
    const add = () => {
        setList([...list, 'new item']);
    };
    const remove = (removeItem) => {
        setList(list.filter(item => item !== removeItem));
    };
    return (
        <div>
            {list.map((item, idx) => (
                <p key={idx}>
                    <span>{item}</span>
                    <button onClick={() => remove(item)}>
                        Remove
                    </button>
                </p>
            ))}
            <button onClick={add}>
                Add item
            </button>
        </div>
    );
}

但对于更复杂些的情况(比如对象数组),这样还是不太方便,不过也没关系后面会有处理这类情况的其它 Hook。

2. useEffect

基础示例

使用 Hook 实现的 函数组件 (function component),其函数本身执行时机相当于 render 函数,执行较早。

对于日常开发中常用的其它生命周期,通常使用 useEffect Hook 实现。这里的 effect ,官方称呼为“副作用”:

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。

(来源链接: https://zh-hans.reactjs.org/docs/hooks-effect.html

它的第一个参数是个回调函数,称之为 副作用函数

function Example() {
    useEffect(() => {
        // 副作用处理
    });
}

依赖数组

这样写的时候,副作用函数会在函数组件的每次 DOM 更新完毕后被调用,相当于类组件生命周期的 componentDidMount + componentDidUpdate

如果需要在其它时机执行副作用函数,就要靠第二个依赖数组字段了。

如果存在依赖数组,React 就会在每次副作用函数执行前,检查依赖数组中的内容。当依赖数组与上次触发时完全没有变化,就会掉过此次执行。

function Example({ name }) {
    const [count, setCount] = useState(0);
    useEffect(() => {
        // 当 count 更新时触发
    }, [count]);
    useEffect(() => {
        // 当 props.name 更新时触发
    }, [name]);
    useEffect(() => {
        // 当 count 或 props.name 更新时触发
    }, [count, name]);
}

依赖数组传空数组或者固定值的时候,每次触发的值都不会变化,所以这个副作用就只会在组件生命周期中执行一次。

function Example() {
    useEffect(() => {
        // 仅当组件创建挂载后触发一次
    }, []);
}

相当于类组件的 componentDidMount 生命周期函数。

清理函数

对于副作用函数,我们还可以在其中返回一个对应的 清理函数

function Example() {
    useEffect(() => {
        // 副作用处理
        return () => {
            // 清理处理
        };
    }, []);
}

清理函数将在当前副作用函数失效、下一个副作用函数设定之前被执行。

上面的例子中,清理函数的执行时机相当于 componentWillUnmount

比如在组件挂载后添加一个对页面滚动做监听处理,并在卸载时清理监听器:

function Example() {
    useEffect(() => {
        const onScroll = () => {
            // Do something.
        };
        window.addEventListener('scroll', onScroll);
        return () => {
            window.removeEventListener('scroll', onScroll);
        };
    });
};

改变原因

为什么要这样设计呢?官方给出了一个例子,就是根据 props 参数订阅数据源时,如果 props 参数发生变化,都需要清理旧订阅注册新订阅。

在类组件的实现中,这需要把对应处理分散在多个生命周期函数中:

class Example extends Component {    
    constructor(props) {
        super(props);
        this.onDataSourceChange = this.onDataSourceChange.bind(this);
    }

    componentDidMount() {
        DataSource.subscribe(
            this.props.dependId,
            this.onDataSourceChange,
        );
    }

    componentDidUpdate(prevProps) {
        DataSource.unsubscribe(
            prevProps.dependId,
            this.onDataSourceChange,
        );
        DataSource.subscribe(
            this.props.dependId,
            this.onDataSourceChange,
        );
    }

    componentWillUnmount() {
        DataSource.unsubscribe(
            this.props.dependId,
            this.onDataSourceChange,
        );
    }

    onDataSourceChange(status) {
        // Do something.
    }
}

可以看到,对于 同一个数据源的处理被分散得七零八落 ,其中 componentDidUpdate 的处理还经常被遗忘,导致一些本不应该产生的 bug。

如果依赖于多个数据源的组件,或者还有其他相同生命周期的处理(如上面页面滚动事件的监听例子),还会让同一类数据源/事件的处理不能收拢到一起,反而因为 发生时机 而被混在其它不同数据源/事件的处理当中。导致 组件编写过程中需要上下跳跃,而且后期维护中代码的阅读难度上升、可重构性下降

而通过 useEffect 实现,只需要放在同一个副作用处理内,再把相关参数放进依赖数组就行了:

function Example({ dependId }) {
    useEffect(() => {
        const onDataSourceChange = () => {
            // Do something.
        };
        DataSource.subscribe(dependId, onDataSourceChange);
        return () => {
            DataSource.unsubscribe(dependId, onDataSourceChange);
        };
    }, [dependId]);
};

只需要这样,不用再做 componentWillUpdate 的额外处理。而且最终同一类逻辑处理被收在同一个 effect 函数中,开发过程中聚焦单一问题,产出代码清晰可读,十分方便代码维护和重构。

可以说是非常方便了。

3. 小结

基础的 React Hook 就是上面的 useStateuseEffect 两个了,使用它们已经可以替代大部分以前使用类组件完成的功能,并且产出代码和执行效率都挺不错的。

不过 React Hook 的设计也不是十全十美,有些问题通过简单例子可能无法体现出来,还需要通过更多使用场景的实践将其暴露出来。其它 Hooks 也将在新的例子中继续说明。

敬请期待~

To be continued.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK