Hooks 之手寫 useTitle

前言

React Hooks 正憑藉其 Function Component 的特性,已經在實際項目中被普遍應用,而對於邏輯是重複且可被複用的組件,藉助第三方 React Hooks 庫來加快開發效率無疑是正確的選擇。html

我在 Github 上選取了 3 個 React Hooks 庫,它們分別是:前端

以上庫中,都包含了 useTitle 這個 hook 函數,調用它能改變當前頁面的文檔標題(document.title),須要注意的是,當調用 useTitle 的組件卸載時,須要將文檔標題還原。react

你能夠先嚐試本身手寫,思考事後,咱們依次來看這三個庫是怎麼進行設計的。webpack

streamich / react-use

react-use 做爲熱度最高的 hooks 庫,早在 18 年由國外開發者開源,發展至今,包含了大量的處理函數,但質量層次不一,爲何我會這麼說,且看下面分析。git

useTitle 爲例,先展現該庫的源碼:github

// src/useTitle.ts

/* eslint-disable */
import { useRef, useEffect } from "react";
export interface UseTitleOptions {
  restoreOnUnmount?: boolean;
}
const DEFAULT_USE_TITLE_OPTIONS: UseTitleOptions = {
  restoreOnUnmount: false,
};
function useTitle( title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS ) {
  const prevTitleRef = useRef(document.title);
  document.title = title;
  useEffect(() => {
    if (options && options.restoreOnUnmount) {
      return () => {
        document.title = prevTitleRef.current;
      };
    } else {
      return;
    }
  }, []);
}

export default typeof document !== "undefined"
  ? useTitle
  : (_title: string) => {};
複製代碼

大體就是 useTitle 在每次調用時,先調用 useRef(document.title) 將初始的 document.title 保存至 prevTitleRef.current 中,隨後修改文檔標題(注意,這是個伏筆)。web

在組件被銷燬時,調用 useEffect 返回的函數,將 document.title 設置成以前保存的標題。數組

有同窗可能會疑惑,爲何能導出一個三元表達式,這是由於 ES Modules 導出的是一個引用,等到真正執行該模塊時,纔會調用三元表達式,從而動態判斷當前應用是否具備 document 對象,具體可查看 利用 webpack 理解 CommonJS 和 ES Modules 的差別瀏覽器

爲了更直觀的體驗,我使用 create-react-app 初始化了一個新項目,並安裝 react-use.app

修改 App.js :

import React, { useState } from "react";
import { useTitle } from "react-use";

const Demo = () => {
  useTitle("Hello world!", {
    restoreOnUnmount: true,
  });

  return <h1>document.title has changed</h1>;
};

export default () => {
  const [showDemo, setShowDemo] = useState(true);

  return (
    <div> <button onClick={() => setShowDemo(!showDemo)}> {showDemo ? "unmount" : "mount"} </button> {showDemo ? <Demo /> : ""} </div>
  );
};
複製代碼

首次加載,顯示 document.title 已被修改(原標題爲 React App,可查看 public/index.html)。

當我點擊按鈕,卸載組件,卻發現標題仍是 Hello world!

這是由於在 index.js 中,使用了嚴格模式:

ReactDOM.render(
  <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ); 複製代碼

當我將包裹在最外層的 <React.StrictMode></React.StrictMode> 註釋後,當組件被卸載時,就能正確顯示初始標題,完整文檔參照 嚴格模式 – React

檢測意外的反作用

從概念上講,React 分兩個階段工做:

  • 渲染 階段會肯定須要進行哪些更改,好比 DOM。在此階段,React 調用 render,而後將結果與上次渲染的結果進行比較。 提交 階段發生在當 React 應用變化時。(對於 React DOM 來講,會發生在 React 插入,更新及刪除 DOM 節點的時候。)在此階段,React 還會調用 componentDidMountcomponentDidUpdate 之類的生命週期方法。

  • 提交 階段一般會很快,但渲染過程可能很慢。所以,即將推出的 concurrent 模式 (默認狀況下未啓用) 將渲染工做分解爲多個部分,對任務進行暫停和恢復操做以免阻塞瀏覽器。這意味着 React 能夠在提交以前屢次調用渲染階段生命週期的方法,或者在不提交的狀況下調用它們(因爲出現錯誤或更高優先級的任務使其中斷)。

嚴格模式不能自動檢測到你的反作用,但它能夠幫助你發現它們,使它們更具肯定性。經過故意重複調用如下函數來實現的該操做:

  • 函數組件體
  • 函數組件經過使用 useState,useMemo 或者 useReducer

也就是說,useTitle 在嚴格模式下,初始化階段和更新階段都會被執行了兩次。

回顧以前的源碼:

function useTitle( title: string, options: UseTitleOptions = DEFAULT_USE_TITLE_OPTIONS ) {
  const prevTitleRef = useRef(document.title);
  document.title = title;
  useEffect(() => {
      ...
  }, []);
}
複製代碼

document.title = title 這個語句具備反作用(side effect),但卻沒包裹在 useEffect() 中,這是不嚴謹的,顯然違背了 React Hooks 的設計初衷。

注意:
這僅適用於開發模式。生產模式下生命週期不會被調用兩次。

alibaba / hooks

ahooks 做爲阿里集團內部沉澱的 Hooks 庫,基於 UI、SideEffect、LifeCycle、State、DOM 等分類提供了經常使用的 Hooks。

話很少上,直接上源碼:

// packages/hooks/src/useTitle/index.ts

import { useEffect, useRef } from "react";

export interface Options {
  restoreOnUnmount?: boolean;
}

const DEFAULT_OPTIONS: Options = {
  restoreOnUnmount: false,
};

function useTitle(title: string, options: Options = DEFAULT_OPTIONS) {
  const titleRef = useRef(document.title);
  document.title = title;

  useEffect(() => {
    if (options && options.restoreOnUnmount) {
      return () => {
        document.title = titleRef.current;
      };
    }
  }, []);
}

export default typeof document !== "undefined"
  ? useTitle
  : (_title: string) => {};
複製代碼

使人失望的是,代碼幾乎與 react-use 如出一致,因此上一節提到的開發模式下的小 bug,依舊是會存在。

對於相同的 useTitle,react-use 的首次 commit 時間是 Oct 27, 2018,而 ahooks 是 Jul 5, 2020,你們也就見仁見智(前端重複造輪子的不良風氣 or KPI 驅使的開源)。

ecomfe / react-hooks

react-hooks 是由百度在實際開發過程的基礎上開源的 hooks 工具集合。

這裏想誇誇百度,不愧是技術的「黃埔軍校」,直接上源碼:

// packages/document-title/src/index.ts

import { useEffect } from "react";

export function useDocumentTitle(title: string) {
  useEffect(() => {
    const previous = document.title;
    document.title = title;
    return () => {
      document.title = previous;
    };
  }, [title]);
}
複製代碼

代碼很是簡潔,它將 document.title = title 置於 useEffect() 中,避免了反作用產生的影響。

previous 常量去保存初始的標題,並在組件卸載時,還原標題。別忘了在 deps 數組中加入 title 變量。

本身寫

但我我的以爲,下面這種寫法是最好的:

import { useEffect } from "react";

export function useTitle(title: string) {
  const prevTitleRef = useRef(title);
  useEffect(() => {
    document.title = title;
    return () => {
      document.title = prevTitleRef.current;
    };
  }, [title]);
}
複製代碼

因爲 useRef 返回的對象存在於當前組件的整個生命週期(The returned object will persist for the full lifetime of the component.),相較於百度的寫法:

  • 便於在函數其餘位置訪問存入的 title(如 useEffect()外,JSX 中)
  • 更加語義化

因爲在 useEffect 中使用到了 prevTitleRef.current,lint 工具會報 react-hooks/exhaustive-deps 警告。
能夠嘗試使用 // eslint-disable-next-line 註釋。

總結

咱們從一個簡簡單單的 useTitle,看到了三個庫之間的差距,總之你須要切實來選擇正確的 library,也不要盲目信任 library。

適合本身的,纔是最好的。

相關文章
相關標籤/搜索