4 个简单又优雅的 React Hooks

1,322 阅读4分钟

原文地址: medium.com/better-prog…
译文地址:github.com/xiao-T/note…
本文版权归原作者所有,翻译仅用于学习。


React 已成为在浏览器中构建应用程序的最流行的库之一。它是由 Facebook 发布开源并免费给开发者使用的。数以百万计的开发者已经用 React 构建了令人惊叹的企业级应用。新版的 Facebook 站点就是使用 React 和 Relay 构建的,同时,我们也看到了一些大型企业也在使用,比如:微软使用 React 构建了 Azure DevOps 应用。

在 2019 年 二月,React v16.8 中引入了 Hook 的功能。它们可以在没有使用类组件的情况下使用 state 和 React 的其它功能。Hook 可以让你抽象一些 state 逻辑,然后,在组件中复用这个 “hook”。

“Hook 让你在无需修改组件结构的情况下复用状态逻辑”— React’s 文档

在平时的工作中,我收集了一些非常有用的自定义的 hooks。

useDebounce

利用这个 hook,你可以延迟一个操作 — 就像正常的延迟执行一个函数一样。这就像:我们在 hook 中定义了一些本地 state,但是,只有延迟的函数运行时才会更新它们。

一个比较适合使用的场景是,假设,有一个变化非常快的值,你需要做出响应,但是,你的响应是异步或者会比较慢,因此,你只需要对最后一刻的值做出响应即可。这里有个演示:

实现方式

import { useState, useEffect } from 'react';

const useDebounce = <T>(value: T, delay: number): T => {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
  
    return () => {
      clearTimeout(handler);
    }
  }, [value, delay]);
  
  return debouncedValue;
}

用例

import React, { useState, useEffect } from 'react';
import useDebounce from './useDebounce';

const Search = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  
  // ✅ Use debounce hook to debounced searchTerm as it is rapidly changing
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  
  // Effect only runs when debouncedSearchTerm changes,
  // and it only does when the user stops typing for more than 500ms
  useEffect(() => {
    if (debouncedSearchTerm) {
      const fetchResults = async () => {
        const response = await fetch(`/search?q=${debouncedSearchTerm}`);
        const json = await response.json();
        setResults(json);
      }
      
      fetchResults();
    }
  }, [debouncedSearchTerm]);
  
  return (
    <>
      <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
      <ul>
        {results.map((result) => <li>{result.title}</li>)}
      </ul>
    </>
  );
}

在上面的示例中,debouncedSearchTerm 只有在用户停止输入 500ms 后才会执行。如果,用户再一次输入,停止输入后会再次执行函数。这样,我们就可以确保不是每次输入都会向服务器发起请求。

useWhenVisible

在实现无限滚动功能时,这也是非常有用的 hook。这个 hook 允许你监控一个指定的元素,当这个元素在可视窗口可见时,就会执行一个你指定的回调。你可以通过这个回调加载更多的元素。

这对图片懒加载或者当用户向下滚动时触发动画也非常有用。

Hook 利用浏览器的 IntersectionObserver API 监控目标元素,当目标元素进入可是区域时,它就会调用开发者提供的回调。

实现

import React, { useEffect } from 'react';

const useWhenVisible = (target: Element | undefined,
                        callback: () => void,
                        root: Element | undefined = document.body) => {
  useEffect(() => {
    if (!target || !root) {
      return;
    }
    
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        callback();
      }
    }, { root });
    
    observer.observe(target);
    
    return () => {
      observer.unobserve(target);
    }
  }, [target, callback, root]);
};

export default useWhenVisible;

用例

import React, { useEffect, useState, useRef } from 'react';
import useWhenVisible from './useWhenVisible';

const TodoList = () => {
  const limit = 25;
  const [offset, setOffset] = useState(0);
  const [todos, setTodos] = useState([]);
  const lastEl = useRef();

  useEffect(() => {
    const fetchTodos = async () => {
      const response = await fetch(`/todos?limit=${limit}&offset=${offset}`);
      const json = await response.json();
      setTodos((prev) => [...prev, ...json]);
    };

    fetchTodos();
  }, [limit, offset]);

  useWhenVisible(lastEl.current, () => {
    setOffset(offset + limit);
  });

  return (
    <ul>
      {todos.map((todo, index, arr) => (
        <li key={todo.id} ref={index === arr.length - 1 ? lastEl : undefined}>
          {todo.title} - Completed: {todo.completed}
        </li>
      ))}
    </ul>
  );
};

从上面的代码中我们可知,当最后一个元素出现在可视区域时就会更新 offset,同时触发 useEffect,加载下一组数据。非常简单吧!

useTimeout

这个 hook 可以让你在需要延迟执行某些事情的时候用声明式的方式使用正常的 setTimeout。这在很多场景都非常有用,用了它之后,你不需在担心因为忘记清除定时器而引起的内存泄漏或者 bug 的问题。

实现

import React, { useEffect, useRef } from 'react';

const useTimeout = (callback: () => void, delay: number | null) => {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    
    if (delay !== null) {
      const id = setTimeout(tick, delay);
      return () => clearTimeout(id);
    }
  }, [delay]);
};

export default useTimeout;

首先,我们为回调创建了一个 ref。这样我们就可以保存最新的一个回调函数。然后,我们设置一个定时器和清除定时器的功能。如果,你不给 delay 参数赋值,这时就不会有任何的任务执行(第15行)。

用例

import React, { useState } from 'react';
import useTimeout from './useTimeout';

const NewsletterBanner = () => {
  // Wait 5 seconds before poppung up banner
  const wait = 5000;
  const [visible, setVisible] = useState(false);

  useTimeout(() => {
    setVisible(true);
  }, wait);

  if (!visible) return null;

  return (
    <div>
      ... some newsletter modal
    </div>
  );
};

如果,以上代码还不明白,我就不知道还有什么能够更加简单的演示这个 hook 的作用了。


useInterval

这个 hook 和 useTimeout 非常相似。我在 hook 中封装了类似 setTimeout 的 API setInterval。它们的实现看起来一摸一样,因此,我会直接展示代码:

实现

import React, { useEffect, useRef } from 'react';

const useInterval = (callback: () => void, delay: number | null) => {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    
    if (delay !== null) {
      const id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
};

export default useInterval;

如果,你打算向服务器轮训刷新一些数据时,useInterval 就非常有用。如果,你不能控制 API,那么,你就无法实现类似 WebSockets API 一样的功能,向客户端推送数据。这时,你只能通过这个 hook 设置一个轮训。

假设,你有一个收集购物清单的应用,当你把一些物品加入到购物车时可以交叉对比。在这种情况下,你必须获取最新的数据,避免添加相同的商品。

用例

import React, { useState, useCallback } from 'react';
import useInterval from './useInterval';

const ShoppingList = () => {
  // Wait 5 seconds before fetching new data
  const POLL_DELAY = 5000;
  const [items, setItems] = useState([]);

  const fetchItems = useCallback(async () => {
    const response = await fetch('/shopping-list/items');
    const json = await response.json();
    setItems(json);
  }, []);

  useEffect(() => {
    // Fetch items from API on mount
    fetchItems();
  }, []);

  useInterval(() => {
    fetchItems();
  }, POLL_DELAY);

  return (
    <ul>
      {items.map((item) => <li>{item.title}</li>)}
    </ul>
  )
};

这个演示非常简单,你可以每 5 秒中轮训一次 API 刷新数据。


总结

我希望你喜欢这些 Hooks,在将来某一天可以使用到它们。

感谢您的阅读!