我写了一个 React hooks 库

518 阅读4分钟

我写了一个 React hooks 库,名称叫做 @lilib/hooks,最近发布了 1.0.0,把它介绍给你们。

背景

我本意是想在 GitHub 上找一个好用的 hooks 库来帮助日常的开发,但是那么多库,没有一个满足心意。react-use 丰富,但异步数据请求的 hook 封装得太简单;react-query 专注于异步请求,但是 API 太复杂,文档也不直观;ahooks 是一个流行的库,但我实在难以接受其接口命名(例如与 useMount 对应的是 useUpdateEffect,而不是 useUpdate);剩余的库基本上都止步于丰富度太低。

优势

与其它库相比,并没有什么优势。@lilib/hooks 只能覆盖常见的开发场景;在处理网络请求时,并不会面面俱到;在接口命名方面,也是很主观的。

特点

  • 支持 React 16.8+、17+ 和 18+。
  • 支持服务端渲染。
  • 支持 React 的严格模式(StrictMode)。
  • 支持 TypeScript。
  • 提供 50+ 钩子以覆盖常见开发业务。
  • 提供全面的测试用例来保证代码质量。

安装

npm install --save @lilib/hooks

文档

lilibraries.github.io/hooks/

场景

生命周期

import React from "react";
import { useMount, useUpdate, useUnmount } from "@lilib/hooks";

function Example(props) {
  useMount(() => {
    // 组件挂载
  })
  
  useUpdate(() => {
    // 依赖更新
  }, [props.someProp])
  
  useUnmount(() => {
    // 组件卸载
  })
}

异步请求

处理异步请求,需要考虑到:相同请求去重、请求出错回退、重试、轮询、数据缓存、自动刷新、重新加载等。@lilib/hooks 提供了三个函数来处理异步请求:

  • useLoad:基础的异步请求钩子,它有大量的选项可供使用。
  • useReload:重新加载 useLoad 的请求,无论它们是否在同一个组件中。
  • useSubmituseLoad 的阉割版,仅仅提供一个便利用于增删改请求。
import React, { FormEvent } from "react";
import { useLoad, usePersist, useReload, useSubmit } from "@lilib/hooks";

function getUsers(): Promise<string[]> {
  return fetch("/users").then((response) => {
    return response.json();
  });
}

function createUser(username: string) {
  return fetch(`/users`, {
    method: "POST",
    body: JSON.stringify({ username }),
  });
}

// 用户列表组件
function UserList() {
  const { data, error, loading } = useLoad(
    getUsers,
    [],
    {
      // 请求 key,useReload 根据此 key 来重新加载请求。
      // 此选项接受 non-nullable 数据,你甚至可以直接将 `getUsers` 函数作为 key。
      key: "users",
      // 初始数据
      initialData: [],
      // 开启轮询
      polling: true,
      // 轮询间隔为一分钟
      pollingInterval: 60000,
    }
  );

  if (error) {
    return <span>出错了!</span>;
  }
  if (loading) {
    return <span>加载中...</span>;
  }

  return (
    <ul>
      {data.map((username) => (
        <li key={username}>{username}</li>
      ))}
    </ul>
  );
}

// 创建用户组件
function CreateUser() {
  const reloadUsers = useReload("users");

  const { submit, submitting } = useSubmit(
    createUser,
    {
      // 新增成功的回调
      onSuccess: () => {
        alert("新增成功");
        reloadUsers(); // 重新加载用户列表
      },
      // 新增失败的回调
      onFailure: () => {
        alert("新增失败");
      },
    }
  );

  // 处理提交事件,通过 `usePersist` 包裹的函数,其指针不会改变
  const handleSubmit = usePersist((event: FormEvent) => {
    event.preventDefault();
    const input = document.getElementById("username") as HTMLInputElement;
    submit(input.value); // 提交
  });

  return (
    <form onSubmit={handleSubmit}>
      <input id="username" placeholder="请输入用户名" />
      <button type="submit">{submitting ? "提交中..." : "提交"}</button>
    </form>
  );
}

function App() {
  return (
    <>
      <CreateUser />
      <UserList />
    </>
  );
}

搜索节流

@lilib/hooks 提供了 useThrottleuseDebounceuseThrottledValueuseDebouncedValue 来节流和防抖。下面示例使用了 useThrottledValue 来实现搜索节流。

import React, { useState } from "react";
import { useThrottledValue, useLoad } from "@lilib/hooks";

function App() {
  const [value, setValue] = useState("");
  const [keyword] = useThrottledValue(value, { wait: 3000 }); // 节流处理

  const { data, error, loading } = useLoad(
    () => {
      return fetch(`/search?keyword=${keyword}`).then((response) => {
        return response.json();
      }) as Promise<string[]>;
    },
    [keyword], // 重新请求的依赖数组
    {
      initialData: [],
    }
  );

  return (
    <div>
      <input
        value={value}
        onChange={(event) => {
          setValue(event.target.value);
        }}
        placeholder="搜索..."
      />

      {error ? (
        "Error"
      ) : loading ? (
        "Loading..."
      ) : (
        <ul>
          {data.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

组件封装

在封装函数组件时,常常需要将 ref 向下传递到真实的 DOM 元素上,如果组件内部也需要使用到 DOM,则可以通过 useComposedRef 来将它们合并。

import React, { forwardRef, ReactNode, useRef } from "react";
import { useComposedRef } from "@lilib/hooks";

interface ComponentProps {
  children?: ReactNode;
}

const Component = forwardRef<HTMLDivElement, ComponentProps>((props, ref) => {
  const domRef = useRef(null);
  const composedRef = useComposedRef(domRef, ref);

  // Do something.

  return <div ref={composedRef}>{props.children}</div>;
});

执行动画

执行 JavaScript 通常会使用 requestAnimationFrame 来提升性能。useAnimation 钩子在回调函数中直接将时间转化为了百分比,方便你使用,且在组件卸载时会自动调用 cancelAnimationFrame,无需手动处理。

import React, { useRef } from "react";
import TweenJS from "@tweenjs/tween.js";
import { useAnimation } from "@lilib/hooks";

function Example() {
  const domRef = useRef<HTMLDivElement>(null);
  const [start, cancel] = useAnimation(
    (percent) => {
      // percent 是一个介于 0 到 1 之间的小数
      if (domRef.current) {
        domRef.current.style.width = percent * 100 + "%";
      }
    },
    {
      duration: 1000,
      algorithm: TweenJS.Easing.Bounce.Out,
    }
  );

  return (
    <>
      <div>
        <button onClick={start}>Start</button>{" "}
        <button onClick={cancel}>Cancel</button>
      </div>
      <div
        ref={domRef}
        style={{ width: 0, height: 8, marginTop: 8, backgroundColor: "orange" }}
      ></div>
    </>
  );
}

功能探测

import React, { useRef } from "react";
import {
  useDarkMode,
  usePageVisible,
  useWindowFocus,
  useIntersecting,
} from "@lilib/hooks";

function Example() {
  // 是否为暗黑模式
  const isDarkMode = useDarkMode();

  // 页面是否可见
  const isPageVisible = usePageVisible();

  // 窗口是否聚焦
  const isWindowFocus = useWindowFocus();

  const rootRef = useRef(null);
  const targetRef = useRef(null);
  // 判断两个元素之间是否相交
  const isIntersecting = useIntersecting(targetRef, { root: rootRef });

  return (
    <div ref={rootRef} style={...}>
      <div ref={targetRef} style={...}>Target</div>
    </div>
  );
}

总结

上面的场景仅占 @lilib/hooks 的一小部分,更多详情请查看文档