聊一聊 React Hook

2,135 阅读6分钟

React Hook

Hook 是什么? React Hook 又是什么?

Hook 中文的意思是钩子。其实,在计算机编辑领域有专业解释。看看来自维基百科的解释:

钩子编程(hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。

处理被拦截的函数调用、事件、消息的代码,被称为钩子(hook)。

React Hook 咱是不是就可以这样理解: react 组件加载、渲染、卸载过程中的拦截处理程序。

React Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:

  1. 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  2. 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)

React Hook 是为了解决什么问题?

咱们得想,程序设计的升级是为了什么?计算机性、人性。

计算机性,就是让计算机更好的执行程序,不要崩溃。

人性,就是怎么让程序员更好的写代码:条理性、易读性、维护性、扩展性!

根据 React 官网的描述:Hook 主要是为了解决组件内状态逻辑的简化和组件间状态逻辑的复用。

  1. 组件内状态逻辑的简化。比如:状态管理的简化,生命周期的简化,这些下面会通过三大基础 hook 来体现。
  2. 组件间状态逻辑的复用,用自定义 hook 就可以体现出来。

常用的 React Hook 有哪些?

我们将用一个 TodoList 项目来简单演示一下,代码地址在文章底部。

useState

赋予函数组件状态管理功能

V16.8.0 版本之前,函数组件被称为无状态组件。只能通过 props 中的数据进行渲染。

而现在,useState 赋予函数组件内部状态管理功能。

  // 初始化一些组件状态
  const [inputItem, setInputItem] = useState("");
  const [listFlag, setListFlag] = useState("all");
  const [allList, setAllList] = useState([]);
  
  // ...
  
  // 通过交互来改变状态
  const handleInputItem = e => {
    setInputItem(e.target.value);
  };
  

是不是很简单!

useEffect

赋予函数组件类似生命周期的功能

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

"在组件第一次渲染和之后更新时,都会执行里面的代码程序。可以设置第二个参数来控制是不是执行,依赖哪些数据变化而执行。"

另外,Effect 分为:需要清除和不需要清除。

改变状态,发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。

订阅外部数据源,时间触发器等,这些需要清除,否则会引起内存泄露!

  1. 不需要做清除副作用
// 初始待办事项列表数据
  useEffect(() => {
    const listData = [
      {
        title: "敲代码",
        isCompleted: false
      },
      {
        title: "写PPT",
        isCompleted: true
      },
      {
        title: "分享",
        isCompleted: false
      }
    ];
    setAllList(listData);
  }, []);  // 一定要注意这里,如果不加第二个参数 [],会导致组件一直不停执行这个副作用里的程序。

  // 设置列表状态
  useEffect(() => {
    setListFlag("activing");
  }, []);
  1. 需要清除副作用
  let timer = null;
  let count = 0;

  // 需要用来证明卸载时执行的程序
  useEffect(() => {
    generateTimer();
    return clearTimer;  //如果不加这句,可能会导致,count 一直会变化,即使组件已经卸载
  }, []);

  const generateTimer = () => {
    timer = setTimeout(() => {
      localStorage.setItem("test-timer", count++);
      generateTimer();
    }, 1000);
  };

  const clearTimer = () => {
    clearTimeout(timer);
  };

useContext

赋予函数组件获取最近上下文的功能

如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>。

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context。

// context.js
import React, { createContext, useState } from "react";

const GlobalContext = createContext();

export default GlobalContext;

// App.js  外层组件
function App() {
  const [username, setUsername] = useState("gpd");

  return (
    <div className="App">
      <GlobalContext.Provider
        value={{ username: username, updateUsername: setUsername }}
      >
      
        <Router>
          <header>
            <ul className="nav-menu">
              <li>
                <Link to="/">Home</Link>
              </li>
              <li>
                <Link to="/todoList">待办事项-原生实现</Link>
              </li>
              <li>
                <Link to="/todoListHook/gpd">待办事项- Hook 实现</Link>
              </li>
            </ul>
          </header>

          <Switch>
            <Route path="/" exact component={Home} />
            <Route path="/todoList" exact component={TodoList} />
            <Route path="/todoListHook/:user" exact component={TodoListHook} />
          </Switch>
        </Router>
      </GlobalContext.Provider>
    </div>
  );
}
import React, { Component, useState, useEffect, useContext } from "react";
import GlobalContext from "../context.js";
import usePageinfo from "../hooks/usePageinfo";
import useTimer from "../hooks/useTimer";

import "./TodoList.css";

const Home = () => {
  const globalContext = useContext(GlobalContext); // 这里进行引用全局 context
  console.log(globalContext);
  usePageinfo();
  const viewTime = useTimer();

  return (
    <div>
      <h1>这是 Home 页面!</h1>
      <h2>页面已经被浏览:{viewTime} 秒!</h2>
      <h2>当前用户是:{globalContext.username}</h2> <!-- 全局 context 里的状态 -->
      <button
        className="update"
        type="button"
        onClick={() => globalContext.updateUsername("sjs")} // 使用全局 context 里更新方法来改变全局状态
      >
        修改用户名为 “sjs”
      </button>
    </div>
  );
};

export default Home;

如何自定义一个 React Hook ?

将多个组件中可复用的逻辑提取到一个函数中。

Demo 项目中写了一个页面被浏览了多长时间的例子。因为,所有的页面组件都共用这个逻辑,所以把它提取了出来。

import React, { Component, useState, useEffect } from "react";

// 返回一个计时时间
const useTimer = () => {
  const [count, setCount] = useState(0);
  let timer = null;

  // 需要用来证明卸载时执行的程序
  useEffect(() => {
    generateTimer();
    return clearTimer;
  }); // 这里就不要第二个参数 [],否则只执行一次后就停止了

  const generateTimer = () => {
    timer = setTimeout(() => {
      setCount(count + 1);
      generateTimer();
    }, 1000);
  };

  const clearTimer = () => {
    clearTimeout(timer);
  };
  return count;
};

export default useTimer;
// pages/Home.js
const Home = () => {
  const globalContext = useContext(GlobalContext);
  console.log(globalContext);
  usePageinfo();
  const viewTime = useTimer(); //看这里,用到自定义组件

  return (
    <div>
      <h1>这是 Home 页面!</h1>
      <h2>页面已经被浏览:{viewTime} 秒!</h2> <!-- 这里用来展示 -->
      <h2>当前用户是:{globalContext.username}</h2>
      <button
        className="update"
        type="button"
        onClick={() => globalContext.updateUsername("sjs")}
      >
        修改用户名为 “sjs”
      </button>
    </div>
  );
};

// pages/TodoListHook.js
const TodoListHook = () => {
  usePageinfo();
  const viewTime = useTimer(); //看这里,用到自定义组件
  //...
  return (
    <div className="todoList">
      <h2>页面已经被浏览:{viewTime} 秒!</h2> <!-- 这里用来展示 -->
      <!-- ...  -->
     </div>
  );
};  

当然,如果展示方式如果完全一样,可以做成一个组件即可。这里的区分就是: hook 一般用来做数据处理。

看一下 React router 提供的 React Hook 。

import React, { Component, useState, useEffect } from "react";
import {
  useParams,
  useHistory,
  useLocation,
  useRouteMatch
} from "react-router-dom";

const usePageinfo = () => {
  const routeParams = useParams();
  const routeHistory = useHistory();
  const routeLocation = useLocation();
  const routeMatch = useRouteMatch();
  console.log("useParams:");
  console.log(routeParams);
  console.log("useHistory:");
  console.log(routeHistory);
  console.log("useLocation:");
  console.log(routeLocation);
  console.log("useMatch:");
  console.log(routeMatch);
};

export default usePageinfo;

可以看到非常简单地就能拿到 params、history、location、match。不用再用 props 一层层传递下来。

回顾一下,本篇文章要讲的。

  1. 咱们介绍了 hook 编程,怎么更好地理解 react hook 。
  2. 简单介绍官方三大基础 hook:useState、useEffect、useContext。
  3. 实现一个自定义 hook,页面浏览时长计时。
  4. 查看了第三方库 react router 怎么运用的 react hook。

欢迎大家拍砖,积极讨论。

项目代码地址:github.com/peidongGuo/…