乐趣区

react hooks初探

hooks 是什么

hooks 是 react16.8 版本中新增的特性,它让我们能够在不写 class 的情况下使用状态和其他 react 特性。也就是说现在我们可以在函数组件中进行状态管理了。

hooks 解决了什么问题

  • 组件中带状态的逻辑很难复用

在 hooks 之前解决这个问题的手段是 render props 和高阶组件。但是这些方法都需要我们去修改组件层级关系,让代码变得很繁琐。
hooks 可以在不改变组件层级的前提下将带有状态的逻辑抽离出来。

  • 复杂组件让人难以理解

原先用 class 写的组件集成了许多生命周期函数,这些生命周期函数中又包含了许多互不相关的逻辑,比如接口请求,事件绑定等等。这导致组件逻辑复杂之后难以理解也难以测试。
hooks 可以将复杂组件的逻辑拆分成更小的函数,这些函数只负责单一的逻辑。拆分后的优点是易懂易测试。

  • class 本身存在的问题

尽管 class 已经是一个语法糖了,但是 react 的开发者认为 this 是一个很麻烦的东西,我们在用 class 写组件前必须先搞清楚 this 的工作原理。另外 class 组件在 react 编译过程中也存在一些问题,如压缩率并不是很好,热加载不稳定等等。
hooks 可以让我们在函数组件中管理状态。尽管完全抛弃 class 组件还为时尚早,但是有了 hooks 我们使用 class 组件的机会将越来越少。

几个常用的 hooks

useState

基本用法:

import React, {useState} from 'react';
export default function Foo() {const [selectedKey, setSelectedKey] = useState('');
    const someHandler = () => { setSelectedKey('1') };
    ...
    return ...;
}

上面例子中,useState 函数会返回一个数组,数组第一项是我们定义的一个状态 selectedKey,第二项是修改这个状态的函数,而 userState 接收的参数就是这个状态的初始值。当我们使用 setSelectedKey 修改状态时,react 会重新渲染该组件,效果跟 setState 一样。
这段代码改成 class 的写法是这样的:

import React, {Component} from 'react';
export default class Foo extends Component {
    state = {selectedKey: ''}
    someHandler = () => {this.setState({selectedKey: '1'});
    }
    ...
    render() {return ...;}
}

区别:

  • 使用 class 时,我们把组件的所有状态都放在 state 这个对象中;而一个 useState 只定义一个状态量,组件有几个状态变量就写几行 useState
  • setState 是将新状态 merge 到老状态中,而 useState 返回的函数 setSelectedKey 是将新状态替换老状态,因为一个 useState 只定义一个状态量所以这边直接替换是没有问题的

useEffect

顾名思义,useEffect 就是去做一些有副作用的事。默认情况下 useEffect 接收一个函数作为参数,在每次 render 结束后 react 会去执行这个函数,效果相当于 componentDidMount 和 componentDidUpdate 的组合。
下面这段代码表示每次渲染后将状态 selectedKey 的值设为 home:

import React, {useState, useEffect} from 'react';
export default function Foo() {const [selectedKey, setSelectedKey] = useState('');
    useEffect(()=> {setSelectedKey('home');
    });
    ...
    return ...;
}

useEffect 的功能还不止这么简单,当我们进行某些副作用操作后,往往需要在组件卸载前做一些清理工作,比如清除定时器,解绑事件监听器等等。因此 react 在 useEffect 中增加了一个特性,允许传入的函数再返回一个函数,这个返回函数的执行时机是下一次触发这个 useEffect 前,以及组件卸载前。
如果我们只想在组件卸载前进行一些清理工作,那就要用到 uesEffect 的第二个参数了。第二个参数是一个数组,里面可以放这个副作用的依赖,作用是控制这个副作用执行的时机,只有当依赖发生变化的时候才会执行这个副作用。当第二个参数为空数组时,返回函数(进行清理工作)只会在组件卸载时执行。
下面这个例子的作用是在组件首次渲染后以及 props.source 的值发生变化后执行 subscribe(),在下一次执行 subscribe()前以及组件卸载前执行 unsubscribe():

useEffect(() => {const subscription = props.source.subscribe();
    return () => {subscription.unsubscribe();
    };
  },
  [props.source]
);

使用 useEffect 可以模拟 react 的某些生命周期函数

模拟 componentDidMount
useEffect(() => {// 这里在 mount 时执行一次}, []);
模拟 componentWillUnmount
useEffect(() => {
    // 这里在 mount 时执行一次
    return () => {// 这里在 unmount 时执行一次}
}, []);
模拟 componentDidUpdate
// useRef 会返回一个对象,这个对象有个 current 属性,值为传给 useRef 的参数
// useRef 在组件生命周期中只初始化一次,之后它会帮我们保存返回的对象
const mounting = useRef(true);
useEffect(() => {if (mounting.current) {mounting.current = false; // 对 current 的修改会被 useRef 保存,但修改不会引起重新渲染} else {// 这里只在 update 时执行}
});

举例

需求:使用 antd 的 menu 组件实现一个侧边栏,类似下面的样子,当用户输入不同 url 时侧边栏需要联动

实现:

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

export default function Sider() {const [selectedKey, setSelectedKey] = useState('');
    const [openKeys, setOpenKeys] = useState([]);
    // 组件更新时根据 url 更新选中的菜单项
    useEffect(() => {const key = getSelectedKey(); // 根据 url 得到选中的菜单项
        setSelectedKey(key);
    });
    // 组件 mount 时根据 url 自动展开子菜单
    useEffect(() => {const key = getOpenKey(); // 根据 url 得到应该展开的菜单
        setOpenKeys([key]);
    }, []);
    ...
    return (
        <Menu
            mode="inline"
            selectedKeys={[selectedKey]}
            openKeys={openKeys}
            onOpenChange={openKeys => setOpenKeys(openKeys)}
        >
        ...
        </Menu>
    );
}

分析:

  • 组件使用 useState 定义了两个状态量:selectedKey 和 openKeys;
  • 第一个 useEffect 用于更新 selectedKey,它会在每次 render 后从 url 中获取当前选中的菜单项,然后更新 selectedKey;
  • 第二个 useEffect 用于首次进入网站时,从 url 中获取应该展开的菜单并更新 openKeys,它只在组件创建时执行,相当于 componentDidMount;
  • 当用户点击父菜单想要展开或收起时,通过 onOpenChange 事件来触发 openKeys 的更新;
  • 当用户点击子菜单想要选中时,会先触发路由跳转(这个逻辑无法从代码中获取,请自行脑补),路由改变会引发改组件重新渲染,继而触发第一个 useEffect 来更新 selectedKey。

自定义 hooks

文章开头已经讲到了 hooks 可以很方便的实现带状态的逻辑复用。
下面是一个简单的自定义 hooks,功能是请求接口并返回数据:

import {useState, useEffect} from 'react';

export default function useUserInfo() {const [userInfo, setUserInfo] = useState(null);
    useEffect(() => {fetch('https://react.hooks.com/api/userinfo').then(
            data => {setUserInfo(data);
            },
        );
    }, []);
    return userInfo;
}

使用也很简单,当 Home 组件加载时会通过 useUserInfo 这个自定义 hooks 去请求接口,接口数据返回时 Home 组件会自动更新:

import React from 'react';
import useUserInfo from '../../hooks/useUserInfo';

export default function Home() {const userInfo = useUserInfo();
    
    return <div>{userInfo}</div>;
}

hooks + context 进行全局状态管理

react 提供了 useContext 这个 hooks 使得在函数组件中使用 context 变得更加方便。
如果项目没有复杂到需要上 redux,可以使用下面的方法进行全局状态管理。
首先创建一个 context:

// globalContext.js
import React from 'react';

export default React.createContext({
    musicianPlan: '1',
    language: 'zh',
    changeMusicianPlan: () => {},
    changeLanguage: () => {},
});

然后定义一个高阶组件,用于管理 context 中的状态:

// globalState.jsx
import React, {useState} from 'react';
import GlobalContext from './globalContext';

export default function GlobalState(props) {const [musicianPlan, setMusicianPlan] = useState('1');
    const [language, setLanguage] = useState('zh');
    const changeMusicianPlan = planId => {setMusicianPlan(planId);
    };
    const changeLanguage = lang => {setLanguage(lang);
    };
    return (
        <GlobalContext.Provider
            value={{
                musicianPlan: musicianPlan,
                language: language,
                changeMusicianPlan: changeMusicianPlan,
                changeLanguage: changeLanguage,
            }}
        >
            {props.children}
        </GlobalContext.Provider>
    );
}

将这个高阶组件放到组件树的顶层:

// app.jsx
import React from 'react';
import GlobalState from './context/globalState';

export default function App() {
    return (
        <GlobalState>
            <Layout>
                ...
            </Layout>
        </GlobalState>
    );
}

在 Header 组件中用 useContext 这个 hooks 获取到 context,然后调用 changeMusicianPlan 方法来改变全局状态 musicianPlan:

import React, {useContext, ReactElement} from 'react';
import {Select} from 'antd';
import GlobalContext from '../../context/globalContext';

const Option = Select.Option;

export default function Header() {const globalContext = useContext(GlobalContext);
    return (
        <Select
            defaultValue="1"
            onChange={value => globalContext.changeMusicianPlan(value)}
        >
            <Option value="1"> 计划一 </Option>
            <Option value="2"> 计划二 </Option>
            <Option value="3"> 计划三 </Option>
        </Select>
    );
}

在 Home 组件中同样使用 useContext 获取 context,然后使用全局状态 musicianPlan 进行动态渲染:

import React, {useContext} from 'react';
import GlobalContext from '../../context/globalContext';

export default function Home() {const globalContext = useContext(GlobalContext);
    return <div>{globalContext.musicianPlan}</div>;
}

当然上面的方法也可以用于某个局部组件树的状态管理,将状态进行拆分管理不仅提高运行效率也更清晰易懂。

使用 hooks 的注意事项

  • hooks 只能在组件内部的最顶层调用,不能将其放在循环语句、条件语句或者子函数内;
  • hooks 只能在函数组件或自定义 hooks 中使用;
退出移动版