您应该开始使用的 5 个 React 自定义 Hooks(已解释)目录 React Hooks useFetch useEventListener useLocalStorage useMediaQuery useDarkMode 结论 成为 React 开发者

2025-05-24

你应该开始使用的 5 个 React 自定义 Hooks(已解释)

目录

React Hooks

useFetch

使用事件监听器

使用本地存储

useMediaQuery

使用暗黑模式

结论

成为一名 React 开发者

你是否在函数式组件中一遍又一遍地重复构建相同的功能?那么,在本视频中,我们将介绍我在大多数 React 应用程序中每天都会用到的 5 个自定义钩子,你也应该尝试一下。

这 5 个 React hooks 将提高您的工作效率,加快您的开发进程,并节省大量时间,以便您可以为您的产品或应用程序开发更有价值的功能。

让我们开始吧!

在Youtube上观看视频或继续阅读。


目录


React Hooks

React Hooks 简介

React Hooks 已在 16.8 版本中引入。它允许你在函数式组件中使用状态和其他 React 特性,这样你甚至不需要再编写类了。

事实上,钩子的作用远不止于此。

钩子让我们将组件内部的逻辑组织成可重复使用的隔离单元。

它们与 React 组件模型和构建应用程序的新方式完美契合。Hooks 可以涵盖类的所有用例,同时在整个应用程序中提取、测试和重用代码方面提供更大的灵活性。

通过构建您自己的自定义 React hooks,您可以轻松地在应用程序的所有组件之间甚至不同的应用程序之间共享功能,这样您就不会重复自己,并且可以更高效地构建 React 应用程序。

现在,我们将看看我的前 5 个自定义钩子,从头开始重新创建它们,以便您真正了解它们的工作原理以及如何使用它们来提高您的工作效率并加快您的开发过程。

那么让我们直接开始构建我们的第一个自定义 React hook。

useFetch

您构建过多少次 React 应用程序需要从外部源获取数据然后才能将其呈现给用户?

每次构建 React 应用时,我都会进行数据获取。我甚至会在单个应用中多次调用数据获取函数。

无论您选择使用AxiosFetch API还是其他任何方式来获取数据,您总是在 React 组件和应用程序中一遍又一遍地编写相同的代码。

因此,让我们看看如何构建一个简单但有用的自定义钩子,当我们需要在应用程序内获取数据时,我们可以调用它。

这样,我们将能够在任何功能组件中重用该 React hook 内的逻辑,只需一行代码即可获取数据。

好的。那么让我们调用我们的自定义钩子:useFetch.

这个钩子接受两个参数,我们需要查询以获取数据的 URL 和一个表示我们想要应用于请求的选项的对象。

import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

获取数据是副作用。所以我们应该使用 React useEffecthooks 来执行查询。

在这个例子中,我们将使用 Fetch API 发出请求。因此,我们将传递 URL 和选项。一旦 Promise 被解析,我们就会通过解析响应主体来检索数据。为此,我们使用了该json()方法。

然后,我们只需要将其存储在 React 状态变量中。

import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch(url, options)
      .then(res => res.json())
      .then(data => setData(data));
  }, [url, options]);
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

好的,但我们也应该捕获并处理网络错误,以防请求出错。所以我们将使用另一个状态变量来存储错误。这样我们就可以从钩子中返回它,并判断是否发生了错误。

import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url, options)
      .then(res => res.json())
      .then(data => {
        if (isMounted) {
          setData(data);
          setError(null);
        }
      })
      .catch(error => {
        if (isMounted) {
          setError(error);
          setData(null);
        }
      });
  }, [url, options]);
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

我们的useFetch钩子将返回一个对象,其中包含从 URL 获取的数据或发生任何错误时的错误。

return { error, data };
Enter fullscreen mode Exit fullscreen mode

最后,向用户指示异步请求的状态通常是一种很好的做法,例如在呈现结果之前显示加载指示器。

因此,让我们在自定义钩子中添加第三个状态变量来跟踪请求的状态。我们将此loading变量在发起请求之前设置为当前值,并在请求完成后将true其设置回当前值。false

const useFetch = (url = '', options = null) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);

    fetch(url, options)
      .then(res => res.json())
      .then(data => {
        setData(data);
        setError(null);
      })
      .catch(error => {
        setError(error);
        setData(null);
      })
      .finally(() => setLoading(false));
  }, [url, options]);

  return { error, data };
};
Enter fullscreen mode Exit fullscreen mode

现在,我们可以将此变量与其他变量一起返回,以便在我们的组件中使用它来在请求运行时呈现加载微调器,以便我们的用户知道我们正在获取他们要求的数据。

return { loading error, data };
Enter fullscreen mode Exit fullscreen mode

在我们了解如何使用新的自定义钩子之前还有一件事。

我们需要检查使用钩子的组件是否仍然挂载,以便更新状态变量。否则,我们的应用程序就会出现内存泄漏。

为此,我们可以简单地创建一个变量来检查组件是否仍然挂载,并在组件卸载时使用 cleanup 函数更新此变量。在 Promise 方法中,我们可以先检查组件是否已挂载,然后再更新状态。

import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let isMounted = true;

    setLoading(true);

    fetch(url, options)
      .then(res => res.json())
      .then(data => {
        if (isMounted) {
          setData(data);
          setError(null);
        }
      })
      .catch(error => {
        if (isMounted) {
          setError(error);
          setData(null);
        }
      })
      .finally(() => isMounted && setLoading(false));

    return () => (isMounted = false);
  }, [url, options]);

  return { loading, error, data };
};

export default useFetch;
Enter fullscreen mode Exit fullscreen mode

好了!现在,让我们看看使用钩子获取数据有多么简单useEffect

我们只需要传递想要检索的资源的 URL。这样,我们就能得到一个可以用来渲染应用程序的对象。

import useFetch from './useFetch';

const App = () => {
  const { loading, error, data = [] } = useFetch(
    'https://hn.algolia.com/api/v1/search?query=react'
  );

  if (error) return <p>Error!</p>;
  if (loading) return <p>Loading...</p>;

  return (
    <div>
      <ul>
        {data?.hits?.map(item => (
          <li key={item.objectID}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

使用事件监听器

让我们进入第二个自定义钩子:useEventListener.

这个钩子负责设置和清理我们组件内的事件监听器。

这样,我们每次需要向应用程序添加事件监听器时就不需要重复操作了。

它接受我们想要监听的事件的名称、指定类型的事件发生时运行的函数、监听事件的目标以及事件监听器的一组选项作为参数。

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {};

export default useEventListener;
Enter fullscreen mode Exit fullscreen mode

与上一个钩子一样,我们将使用 ReactuseEffect钩子来添加事件监听器。但首先,我们需要确保目标支持这些addEventListener方法。否则,我们什么也不做!

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {

  useEffect(() => {
    if (!target?.addEventListener) return;
  }, [target]);
};

export default useEventListener;
Enter fullscreen mode Exit fullscreen mode

然后,我们可以添加实际的事件监听器并在清理函数中将其删除。

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {
  useEffect(() => {
    if (!target?.addEventListener) return;

    target.addEventListener(eventType, listener, options);

    return () => {
      target.removeEventListener(eventType, listener, options);
    };
  }, [eventType, target, options, listener]);
};

export default useEventListener;
Enter fullscreen mode Exit fullscreen mode

实际上,我们还将使用一个引用对象来存储并持久化监听函数,使其在渲染过程中保持持久化。只有当监听函数发生变化时,我们才会更新此引用,并在事件监听方法中使用此引用。

import { useEffect, useRef } from 'react';

const useEventListener = (
  eventType = '',
  listener = () => null,
  target = null,
  options = null
) => {
  const savedListener = useRef();

  useEffect(() => {
    savedListener.current = listener;
  }, [listener]);

  useEffect(() => {
    if (!target?.addEventListener) return;

    const eventListener = event => savedListener.current(event);

    target.addEventListener(eventType, eventListener, options);

    return () => {
      target.removeEventListener(eventType, eventListener, options);
    };
  }, [eventType, target, options]);
};

export default useEventListener;
Enter fullscreen mode Exit fullscreen mode

我们不需要从这个钩子返回任何东西,因为我们只是监听事件并运行作为参数传入的处理程序函数。

现在可以轻松地为我们的组件添加事件监听器,例如下面的组件,以检测 DOM 元素外部的点击。这里,如果用户点击了对话框组件外部,我们将关闭对话框组件。

import { useRef } from 'react';
import ReactDOM from 'react-dom';
import { useEventListener } from './hooks';

const Dialog = ({ show = false, onClose = () => null }) => {
  const dialogRef = useRef();

  // Event listener to close dialog on click outside element
  useEventListener(
    'mousedown',
    event => {
      if (event.defaultPrevented) {
        return; // Do nothing if the event was already processed
      }
      if (dialogRef.current && !dialogRef.current.contains(event.target)) {
        console.log('Click outside detected -> closing dialog...');
        onClose();
      }
    },
    window
  );

  return show
    ? ReactDOM.createPortal(
        <div className="fixed inset-0 z-9999 flex items-center justify-center p-4 md:p-12 bg-blurred">
          <div
            className="relative bg-white rounded-md shadow-card max-h-full max-w-screen-sm w-full animate-zoom-in px-6 py-20"
            ref={dialogRef}
          >
            <p className="text-center font-semibold text-4xl">
              What's up{' '}
              <span className="text-white bg-red-500 py-1 px-3 rounded-md mr-1">
                YouTube
              </span>
              ?
            </p>
          </div>
        </div>,
        document.body
      )
    : null;
};

export default Dialog;
Enter fullscreen mode Exit fullscreen mode

使用本地存储

对于我们的第三个自定义钩子,我们将利用localStorage浏览器的功能来跨会话保持组件的状态。

对于这个,我们需要创建或更新的键的名称localStorage以及初始值。就是这样!

import { useState } from 'react';

const useLocalStorage = (key = '', initialValue = '') => {};

export default useLocalStorage;

Enter fullscreen mode Exit fullscreen mode

我们将返回一个类似于 React useStatehook 的数组。因此,该数组将包含一个状态值和一个用于在持久化过程中更新它的函数。localStorage.

让我们开始吧。

首先,让我们创建要同步的 React 状态变量localStorage.

import { useState } from 'react';

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

export default useLocalStorage;
Enter fullscreen mode Exit fullscreen mode

这里我们使用延迟初始化来读取“localStorage”来获取键的值,如果找到任何值则解析该值,或者返回作为第二个参数传递给我们的钩子的初始值。

如果读取时出现错误localStorage,我们只需记录错误并返回初始值。

最后,我们需要创建更新函数来返回将存储任何状态的更新,localStorage而不是使用钩子返回的默认更新useState

import { useState } from 'react';

const useLocalStorage = (key = '', initialValue = '') => {
  const [state, setState] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setLocalStorageState = newState => {
    try {
      const newStateValue =
        typeof newState === 'function' ? newState(state) : newState;
      setState(newStateValue);
      window.localStorage.setItem(key, JSON.stringify(newStateValue));
    } catch (error) {
      console.error(`Unable to store new value for ${key} in localStorage.`);
    }
  };

  return [state, setLocalStorageState];
};

export default useLocalStorage;
Enter fullscreen mode Exit fullscreen mode

此函数同时更新 React 状态和相应的键/值,请localStorage.注意,我们还可以支持像常规useState钩子一样的功能更新。

最后,我们返回状态值和自定义更新函数。

现在,我们可以使用useLocalStorage钩子来保存组件中的任何数据localStorage.

在下面的例子中,我们使用它来存储已连接用户的应用程序设置。

import { useLocalStorage } from './hooks';

const defaultSettings = {
  notifications: 'weekly',
};

function App() {
  const [appSettings, setAppSettings] = useLocalStorage(
    'app-settings',
    defaultSettings
  );

  return (
    <div className="h-full w-full flex flex-col justify-center items-center">
      <div className="flex items-center mb-8">
        <p className="font-medium text-lg mr-4">Your application's settings:</p>

        <select
          value={appSettings.notifications}
          onChange={e =>
            setAppSettings(settings => ({
              ...settings,
              notifications: e.target.value,
            }))
          }
          className="border border-gray-900 rounded py-2 px-4 "
        >
          <option value="daily">daily</option>
          <option value="weekly">weekly</option>
          <option value="monthly">monthly</option>
        </select>
      </div>

      <button
        onClick={() => setAppSettings(defaultSettings)}
        className="rounded-md shadow-md py-2 px-6 bg-red-500 text-white uppercase font-medium tracking-wide text-sm leading-8"
      >
        Reset settings
      </button>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

useMediaQuery

好的!让我们继续讨论第四个 React Hook useMediaQuery,。

这个钩子将帮助我们在功能组件内部以编程方式测试和监控媒体查询。例如,当你需要根据设备类型或特定特性渲染不同的 UI 时,这非常有用。

所以我们的钩子接受 3 个参数,它们是:

  • 首先,对应于媒体查询的字符串数组
  • 然后,与这些媒体查询匹配的值数组,其顺序与前一个数组相同
  • 最后,如果没有媒体查询匹配,则使用默认值
import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

在这个钩子中,我们做的第一件事是为每个匹配的媒体查询构建一个媒体查询列表。我们将使用这个数组来匹配媒体查询并获取相应的值。

import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {
  const mediaQueryList = queries.map(q => window.matchMedia(q));
};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

为此,我们在useCallback钩子内部创建了一个回调函数。我们检索列表中第一个匹配的媒体查询的值,如果均不匹配,则返回默认值。

import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {
  const mediaQueryList = queries.map(q => window.matchMedia(q));

  const getValue = useCallback(() => {
    const index = mediaQueryList.findIndex(mql => mql.matches);
    return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
  }, [mediaQueryList, values, defaultValue]);
};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

然后,我们创建一个 React 状态来存储匹配的值,并使用上面定义的函数对其进行初始化。

import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {
  const mediaQueryList = queries.map(q => window.matchMedia(q));

  const getValue = useCallback(() => {
    const index = mediaQueryList.findIndex(mql => mql.matches);
    return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
  }, [mediaQueryList, values, defaultValue]);

  const [value, setValue] = useState(getValue);
};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

最后,我们在钩子中添加一个事件监听器,useEffect用于监听每个媒体查询的变化。当发生变化时,我们会运行更新函数。

这里我们不会忘记清理所有事件监听器并从钩子中返回状态值。

import { useState, useCallback, useEffect } from 'react';

const useMediaQuery = (queries = [], values = [], defaultValue) => {
  const mediaQueryList = queries.map(q => window.matchMedia(q));

  const getValue = useCallback(() => {
    const index = mediaQueryList.findIndex(mql => mql.matches);
    return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
  }, [mediaQueryList, values, defaultValue]);

  const [value, setValue] = useState(getValue);

  useEffect(() => {
    const handler = () => setValue(getValue);
    mediaQueryList.forEach(mql => mql.addEventListener('change', handler));

    return () =>
      mediaQueryList.forEach(mql => mql.removeEventListener('change', handler));
  }, [getValue, mediaQueryList]);

  return value;
};

export default useMediaQuery;
Enter fullscreen mode Exit fullscreen mode

我最近用过一个简单的例子,就是添加一个媒体查询来检查设备是否允许用户将鼠标悬停在元素上。这样,如果用户可以悬停,我就添加特定的不透明度样式;否则,就应用基本样式。

import { useMediaQuery } from './hooks';

function App() {
  const canHover = useMediaQuery(
    // Media queries
    ['(hover: hover)'],
    // Values corresponding to the above media queries by array index
    [true],
    // Default value
    false
  );

  const canHoverClass = 'opacity-0 hover:opacity-100 transition-opacity';
  const defaultClass = 'opacity-100';

  return (
    <div className={canHover ? canHoverClass : defaultClass}>Hover me!</div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

使用暗黑模式

好了,伙计们!还差一个钩子。

这是我最喜欢的。它允许我轻松快速地将暗黑模式功能应用到我的任何 React 应用程序中。

AlterClass - 带有自定义 React hook 的暗黑模式

让我们看看如何构建这样的钩子。

此钩子旨在按需启用和禁用暗模式,将当前状态存储在localStorage.

为此,我们将使用刚刚构建的两个钩子:useMediaQueryuseLocalStorage.

我们useMediaQuery,可以检查用户对浏览器暗模式的偏好。

然后,使用“useLocalStorage”,我们可以初始化、存储并保存当前状态(暗模式或亮模式)localStorage.

import { useEffect } from 'react';
import useMediaQuery from './useMediaQuery';
import useLocalStorage from './useLocalStorage';

const useDarkMode = () => {
  const preferDarkMode = useMediaQuery(
    ['(prefers-color-scheme: dark)'],
    [true],
    false
  );
};

export default useDarkMode;
Enter fullscreen mode Exit fullscreen mode

最后,这个钩子的最后一部分是触发一个副作用,dark为 * 元素添加或移除类*document.body。这样,我们就可以轻松地将暗色样式应用到我们的应用中了。

import { useEffect } from 'react';
import useMediaQuery from './useMediaQuery';
import useLocalStorage from './useLocalStorage';

const useDarkMode = () => {
  const preferDarkMode = useMediaQuery(
    ['(prefers-color-scheme: dark)'],
    [true],
    false
  );

  const [enabled, setEnabled] = useLocalStorage('dark-mode', preferDarkMode);

  useEffect(() => {
    if (enabled) {
      document.body.classList.add('dark');
    } else {
      document.body.classList.remove('dark');
    }
  }, [enabled]);

  return [enabled, setEnabled];
};

export default useDarkMode;
Enter fullscreen mode Exit fullscreen mode

如果你正在寻找一种简单的方法,那么再次看看支持暗黑模式的 Tailwind CSS。结合这个钩子,Tailwind CSS 成为在任何 React 应用程序中实现暗黑模式最简单、最快捷的方法。

结论

好了!就这样吧,各位。非常感谢你们观看(或阅读)这篇文章。

我真心希望这段视频对你有用。请务必查看Github 仓库,获取我们刚刚构建的所有钩子的源代码。

请与您的朋友分享此视频,点击“赞”按钮,别忘了在 YouTube 上订阅

成为一名 React 开发者

如果您需要了解有关使用 React 构建现代 Web 应用程序的更多信息,请查看我在 AlterClass.io 上的课程

AlterClass.io

我的课程将教您掌握 React、成为一名成功的 React 开发人员并获得聘用所需的一切!

AlterClass.io

我将教授您使用 React 所需的所有概念,您将通过测验和编程评估获得大量的实践练习,并且您将自己构建真实世界的项目。

AlterClass.io

AlterClass.io

此外,您将成为不断壮大的学习者社区的一部分。

因此,请访问 AlterClass.io,注册我的课程,并开始构建强大的 React 应用程序的惊人组合。

文章来源:https://dev.to/alterclass/5-react-custom-hooks-you-should-start-using-explained-5d18
PREV
使用 React 和 Firebase 在 5 分钟内构建一个实时聊天应用程序 目录 我们正在构建什么?创建一个 React 应用程序 创建一个 Firebase 项目 定义 UI Google 身份验证 从 Firestore 实时读取数据 将数据添加到 Firestore 奖励 下一步是什么? 结论 成为一名 React 开发者
NEXT
你应该学习掌握 React 的 10 个 JavaScript 概念目录箭头函数默认参数模板文字 Let 和 Const 类解构三元运算符导入/导出模块异步/等待扩展运算符/剩余参数结论