使用 hooks 让你的 react 项目起飞

445 阅读8分钟

在 React 中,自定义 hooks 是扩展组件功能的强大方式。它们可以让你复用状态逻辑,而不必重复代码或将逻辑放在类组件中。本文介绍一些我收集到的好用的 hooks ,它们涉及页面绘制的方方面面,希望能对你有所帮助。

1. useDebounce

这个 hook 可以用于实现输入框的防抖功能,避免在用户输入时频繁触发回调。

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timeoutId = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timeoutId);
  }, [value, delay]);

  return debouncedValue;
}

测试代码:

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

function DebouncedInput() {
  const [inputValue, setInputValue] = useState('');
  const debouncedValue = useDebounce(inputValue, 500); // 设置防抖延迟为500毫秒  

  return (
    <div
      style={{
        margin: '100px auto',
        width: 'min-content'
      }}
    >
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="Type something..."
      />
      <p>Debounced Value: {debouncedValue}</p>
    </div>
  );
}

export default DebouncedInput;

页面上的效果:

de.gif

2. useClickOutside

这个 hook 可以帮助你检测用户是否点击了组件外部,常用于下拉菜单、模态框等组件。

import { useEffect } from 'react';

function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) {
        return;
      }
      handler(event);
    };
    document.addEventListener('mousedown', listener);
    document.addEventListener('touchstart', listener);

    return () => {
      document.removeEventListener('mousedown', listener);
      document.removeEventListener('touchstart', listener);
    };
  }, [ref, handler]);
}

测试代码:

import React, { useRef, useState } from 'react';  
import useClickOutside from './useClickOutside';  
  
function DropdownMenu() {  
  const [isOpen, setIsOpen] = useState(false);  
  const dropdownRef = useRef(null);  
  
  useClickOutside(dropdownRef, () => setIsOpen(false));  
  
  return (  
    <div
      style={{
        margin: '100px auto',
        width: 'max-content'
      }}
    >  
      <button onClick={() => setIsOpen(!isOpen)}>Toggle Dropdown</button>  
      {isOpen && (  
        <div ref={dropdownRef} className="dropdown-menu">  
          <ul>  
            <li>Option 1</li>  
            <li>Option 2</li>  
            <li>Option 3</li>  
          </ul>  
        </div>  
      )}  
    </div>  
  );  
}  
  
export default DropdownMenu;

在这个例子中,你需要自己添加适当的 CSS 样式来使下拉菜单看起来像一个真正的下拉菜单,并处理选项的选择逻辑。

页面上的效果:

toggle.gif

3. useKeyPress

这个 hook 可以检测键盘上特定键的按下事件。

import { useEffect } from 'react';

function useKeyPress(targetKey) {
  const downHandler = ({ key }) => {
    if (key === targetKey) {
      console.log('Pressed:', key);
    }
  };

  useEffect(() => {
    window.addEventListener('keydown', downHandler);
    return () => {
      window.removeEventListener('keydown', downHandler);
    };
  }, [targetKey]);
}

测试代码:

import React, { useEffect, useState } from 'react';  
import useKeyPress from './useKeyPress';  
  
function KeyPressListener() {  
  const [message, setMessage] = useState('');  
  
  useKeyPress('Escape'); // 监听 Escape 键  
  
  useEffect(() => {  
    // 假设我们想在按下 Escape 键时显示一条消息  
    if (message === 'Pressed: Escape') {  
      alert('Escape key was pressed!');  
      setMessage(''); // 清空消息,避免重复弹出警告  
    }  
  }, [message]); // 依赖 message 变化来触发副作用  
  
  return (  
    <div>  
      <p>Press the Escape key to see an alert.</p>  
      <p>{message}</p>  
    </div>  
  );  
}  
  
// 注意:在实际应用中,通常不会直接在组件中弹出警告,这里只是为了示例。  
export default KeyPressListener;

在这个例子中,useKeyPress hook 被用于监听 Escape 键的按下事件,当 Escape 键被按下时,控制台会打印一条消息,并且通过 useEffect 触发一个警告弹窗。在真实的应用中,你可能会有更复杂的逻辑来处理键盘事件。同时,请注意 useKeyPress hook 需要在真实的应用中进行修改,以便能够触发组件内的状态更新或其他逻辑,而不是仅仅在控制台打印消息。在这个组件示例中,我添加了一个状态 message 来模拟这一行为。

4. useLocalStorage

这个 hook 可以用于在组件状态和本地存储之间同步数据。

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.log(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.log(error);
    }
  };

  useEffect(() => {
    setValue(storedValue);
  }, [key]);

  return [storedValue, setValue];
}

测试代码:

import React from 'react';  
import useLocalStorage from './useLocalStorage';  
  
function App() {  
  const [name, setName] = useLocalStorage('name', '');  
  
  return (  
    <div>  
      <input  
        type="text"  
        value={name}  
        onChange={(e) => setName(e.target.value)}  
        placeholder="Enter your name"  
      />  
      <p>Hello, {name}!</p>  
    </div>  
  );  
}  
  
export default App;

页面效果:

  • 当用户首次访问页面时,输入框为空,下方文字显示为 "Hello, !"。
  • 当用户在输入框中输入名字并失去焦点或按回车键后,该名字将被保存在本地存储中,并且页面上的文本会更新为 "Hello, [输入的名字]!"。
  • 当用户刷新页面或重新打开页面后,之前输入的名字会被自动填充到输入框中,并且下方的问候语也会相应地更新。

loca.gif

5. usePrevious

这个 hook 可以获取上一次的值,常用于性能优化。

import { useEffect, useRef } from 'react';

function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

测试代码:

import React, { useState } from 'react';  
import usePrevious from './usePrevious';  
  
function App() {  
  const [count, setCount] = useState(0);  
  const prevCount = usePrevious(count);  
  
  return (  
    <div>  
      <p>Current Count: {count}</p>  
      <p>Previous Count: {prevCount}</p>  
      <button onClick={() => setCount(count + 1)}>Increment</button>  
    </div>  
  );  
}  
  
export default App;

页面效果:

  • 页面上会显示当前的计数(Current Count)和上一次的计数(Previous Count)。
  • 当用户点击 "Increment" 按钮时,Current Count 会增加,而 Previous Count 会显示为上一次的 Current Count 值。
  • 例如,如果 Current Count 从 0 增加到 1,Previous Count 将显示为 0,直到 Current Count 再次变化。

prev.gif

6. usePagination

这个 hook 可以用于实现分页功能。

import { useState } from 'react';

function usePagination(totalItems, itemsPerPage) {
  const [currentPage, setCurrentPage] = useState(1);

  const items = Math.ceil(totalItems / itemsPerPage);
  const maxPage = items > 0 ? items : 1;

  const changePage = (pageNumber) => {
    if (pageNumber > 0 && pageNumber <= maxPage) {
      setCurrentPage(pageNumber);
    }
  };

  const prevPage = () => {
    if (currentPage > 1) {
      changePage(currentPage - 1);
    }
  };

  const nextPage = () => {
    if (currentPage < maxPage) {
      changePage(currentPage + 1);
    }
  };

  return { currentPage, changePage, prevPage, nextPage };
}

测试代码:

import React, { useState } from 'react';  
import usePagination from './usePagination';  
  
function App() {  
  const [currentPage, changePage] = useState(1);  
  const { currentPage: paginatedCurrentPage, changePage: paginatedChangePage, prevPage, nextPage } = usePagination(100, 10);  
  
  return (  
    <div>  
      <p>Current Page: {paginatedCurrentPage}</p>  
      <button onClick={() => prevPage()}>Previous</button>  
      <button onClick={() => nextPage()}>Next</button>  
      <input type="number" min="1" value={currentPage} onChange={(e) => changePage(Number(e.target.value))} />  
      <button onClick={() => paginatedChangePage(currentPage)}>Go to Page</button>  
    </div>  
  );  
}  
  
export default App;

页面效果:

  • 页面上会显示当前的页码(Current Page)。
  • 用户可以点击 "Previous" 和 "Next" 按钮来浏览不同的页面。
  • 用户还可以在输入框中输入想要跳转的页码,并点击 "Go to Page" 按钮来直接跳转到指定页面。
  • 例如,如果用户点击 "Next" 按钮,Current Page 的值会增加,显示下一页的内容(在实际应用中,你需要根据这个页码来加载相应的数据)。

page.gif

7. useTitle

这个 hook 可以自动更新页面的标题,通常用于反映页面内容或状态。

import { useEffect } from 'react';

function useTitle(title) {
  useEffect(() => {
    document.title = title;
  }, [title]);
}

测试代码:

import React from 'react';  
import useTitle from './useTitle';  
  
function App() {  
  const pageTitle = "My Custom Page Title";  
  useTitle(pageTitle);  
  
  return (  
    <div>  
      <h1>Welcome to the Page!</h1>  
      <p>Check the browser tab title.</p>  
    </div>  
  );  
}  
  
export default App;

页面效果:

  • 当用户访问该页面时,浏览器的标签页标题会自动更新为 "My Custom Page Title"。

8. useMediaQuery

这个 hook 可以响应媒体查询的变化,常用于响应式设计。

import { useState, useEffect } from 'react';

function useMediaQuery(query) {
  const [matches, setMatches] = useState(false);

  useEffect(() => {
    const mediaQueryList = window.matchMedia(query);
    setMatches(mediaQueryList.matches);

    const listener = (event) => {
      setMatches(event.matches);
    };

    mediaQueryList.addListener(listener);

    return () => mediaQueryList.removeListener(listener);
  }, [query]);

  return matches;
}

测试代码:

import React from 'react';  
import useMediaQuery from './useMediaQuery';  
  
function App() {  
  const isMobile = useMediaQuery('(max-width: 600px)');  
  
  return (  
    <div>  
      <h1>Responsive Design Example</h1>  
      <p>Is Mobile: {isMobile ? 'Yes' : 'No'}</p>  
    </div>  
  );  
}  
  
export default App;

页面效果:

  • 当用户访问页面时,会根据屏幕宽度显示是否为移动设备。如果屏幕宽度小于或等于600px,则会显示 "Is Mobile: Yes",否则会显示 "Is Mobile: No"。
  • 如果用户调整浏览器窗口大小或改变设备的方向(例如,从横屏转为竖屏),显示的内容会实时更新以反映当前屏幕宽度是否满足媒体查询条件。

image.png

image.png

9. useAsync

这个 hook 用于执行异步操作,并管理加载和错误状态。

import { useState, useEffect } from 'react';

function useAsync(asyncFunction, dependencies) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [value, setValue] = useState();

  useEffect(() => {
    const runAsync = async () => {
      setLoading(true);
      try {
        const result = await asyncFunction();
        setValue(result);
      } catch (e) {
        setError(e);
      } finally {
        setLoading(false);
      }
    };

    runAsync();
  }, dependencies);

  return { loading, error, value };
}

测试代码:

import React from 'react';  
import useAsync from './useAsync';  
  
// 模拟异步请求数据的函数  
async function fetchData() {  
  return new Promise((resolve) => {  
    setTimeout(() => {  
      resolve('Data fetched successfully!');  
    }, 2000);  
  });  
}  
  
function App() {  
  const { loading, error, value } = useAsync(fetchData, []);  
  
  return (  
    <div>  
      {loading && <p>Loading...</p>}  
      {error && <p>Error: {error.message}</p>}  
      {value && <p>Data: {value}</p>}  
    </div>  
  );  
}  
  
export default App;

页面效果:

  • 当组件挂载时,会显示 "Loading...",表示异步操作正在进行中。
  • 两秒后,异步操作完成,如果成功获取数据,则会显示 "Data: Data fetched successfully!"。
  • 如果在异步操作中发生错误(在上面的示例中没有模拟错误情况),则会显示错误信息,例如 "Error: [error message]"。

async.gif

10. useHover

这个 hook 可以检测元素是否被鼠标悬停。

import { useState, useEffect } from 'react';

function useHover() {
  const [value, setValue] = useState(false);

  const ref = useRef(null);

  const handleMouseOver = () => setValue(true);
  const handleMouseOut = () => setValue(false);

  useEffect(() => {
    const element = ref.current;
    element.addEventListener('mouseover', handleMouseOver);
    element.addEventListener('mouseout', handleMouseOut);

    return () => {
      element.removeEventListener('mouseover', handleMouseOver);
      element.removeEventListener('mouseout', handleMouseOut);
    };
  }, []);

  return [ref, value];
}

测试代码:

import React from 'react';  
import useHover from './useHover';  
  
function HoverComponent() {  
  const [hoverRef, isHovered] = useHover();  
  
  return (  
    <div>  
      <div ref={hoverRef}>  
        Hover over me!  
      </div>  
      {isHovered && <p>The element is being hovered!</p>}  
    </div>  
  );  
}  
  
export default HoverComponent;

页面效果:

  • 当用户将鼠标悬停在 "Hover over me!" 文本上时,会显示 "The element is being hovered!" 文本。
  • 当用户将鼠标移开时,"The element is being hovered!" 文本会消失。

hover.gif

11. useBeforeunload

这个 hook 可以监听浏览器的beforeunload事件,常用于表单提交或长时间运行的任务时提醒用户。

import { useEffect } from 'react';

function useBeforeunload(message) {
  useEffect(() => {
    const handleBeforeUnload = (event) => {
      // 对于大多数浏览器,您需要使用空字符串作为默认提示
      const eventCopy = event || window.event;
      eventCopy.returnValue = message;
      return message;
    };

    window.addEventListener('beforeunload', handleBeforeUnload);

    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, [message]);
}

测试代码:

import React, { useState } from 'react';  
import useBeforeunload from './useBeforeunload';  
  
function UnloadComponent() {  
  const [text, setText] = useState('');  
  
  useBeforeunload('Are you sure you want to leave? You may lose unsaved changes.');  
  
  const handleChange = (event) => {  
    setText(event.target.value);  
  };  
  
  return (  
    <div>  
      <textarea value={text} onChange={handleChange} />  
      <p>Try to reload or close the page after typing something in the textarea.</p>  
    </div>  
  );  
}  
  
export default UnloadComponent;

页面效果:

  • 当用户在文本框中输入文本并尝试重新加载页面或关闭浏览器标签页时,会弹出一个带有自定义消息"Are you sure you want to leave? You may lose unsaved changes."的确认框,询问用户是否确定要离开页面。

dom.gif

12. useInViewport

这个 hook 可以检测元素何时进入或离开视口。

import { useState, useEffect } from 'react';

function useInViewport(ref, rootMargin = '0px', threshold = 0) {
  const [isIntersecting, setIntersecting] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setIntersecting(entry.isIntersecting);
      },
      { rootMargin, threshold }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => {
      if (ref.current) {
        observer.unobserve(ref.current);
      }
    };
  }, [ref, rootMargin, threshold]);

  return isIntersecting;
}

测试代码:

import React, { useRef } from 'react';  
import useInViewport from './useInViewport';  
  
function ViewportComponent() {  
  const elementRef = useRef(null);  
  const isInViewport = useInViewport(elementRef);  
  
  return (  
    <div style={{ marginBottom: 1000}}>  
      <div ref={elementRef} style={{ height: '500px', backgroundColor: 'lightblue' }}>  
        Scroll me into view!  
      </div>  
      {<p  style={{ marginTop: 100}}>{isInViewport ? 'The element is in the viewport!' : 'The element isn"t in the viewport!'}</p>}  
    </div>  
  );  
}  
  
export default ViewportComponent;

页面效果:

  • 当用户滚动页面,使得 "Scroll me into view!" 文本区域进入视口时,会显示 "The element is in the viewport!" 文本。
  • 当该文本区域离开视口时,"The element is in the viewport!" 文本会消失。

view.gif