你应该开始使用的 5 个 React 自定义 Hooks(已解释)
目录
React Hooks
useFetch
使用事件监听器
使用本地存储
useMediaQuery
使用暗黑模式
结论
成为一名 React 开发者
你是否在函数式组件中一遍又一遍地重复构建相同的功能?那么,在本视频中,我们将介绍我在大多数 React 应用程序中每天都会用到的 5 个自定义钩子,你也应该尝试一下。
这 5 个 React hooks 将提高您的工作效率,加快您的开发进程,并节省大量时间,以便您可以为您的产品或应用程序开发更有价值的功能。
让我们开始吧!
在Youtube上观看视频或继续阅读。
目录
React Hooks
React Hooks 已在 16.8 版本中引入。它允许你在函数式组件中使用状态和其他 React 特性,这样你甚至不需要再编写类了。
事实上,钩子的作用远不止于此。
钩子让我们将组件内部的逻辑组织成可重复使用的隔离单元。
它们与 React 组件模型和构建应用程序的新方式完美契合。Hooks 可以涵盖类的所有用例,同时在整个应用程序中提取、测试和重用代码方面提供更大的灵活性。
通过构建您自己的自定义 React hooks,您可以轻松地在应用程序的所有组件之间甚至不同的应用程序之间共享功能,这样您就不会重复自己,并且可以更高效地构建 React 应用程序。
现在,我们将看看我的前 5 个自定义钩子,从头开始重新创建它们,以便您真正了解它们的工作原理以及如何使用它们来提高您的工作效率并加快您的开发过程。
那么让我们直接开始构建我们的第一个自定义 React hook。
useFetch
您构建过多少次 React 应用程序需要从外部源获取数据然后才能将其呈现给用户?
每次构建 React 应用时,我都会进行数据获取。我甚至会在单个应用中多次调用数据获取函数。
无论您选择使用Axios、Fetch API还是其他任何方式来获取数据,您总是在 React 组件和应用程序中一遍又一遍地编写相同的代码。
因此,让我们看看如何构建一个简单但有用的自定义钩子,当我们需要在应用程序内获取数据时,我们可以调用它。
这样,我们将能够在任何功能组件中重用该 React hook 内的逻辑,只需一行代码即可获取数据。
好的。那么让我们调用我们的自定义钩子:useFetch.
这个钩子接受两个参数,我们需要查询以获取数据的 URL 和一个表示我们想要应用于请求的选项的对象。
import { useState, useEffect } from 'react';
const useFetch = (url = '', options = null) => {};
export default useFetch;
获取数据是副作用。所以我们应该使用 React useEffect
hooks 来执行查询。
在这个例子中,我们将使用 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;
好的,但我们也应该捕获并处理网络错误,以防请求出错。所以我们将使用另一个状态变量来存储错误。这样我们就可以从钩子中返回它,并判断是否发生了错误。
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;
我们的useFetch
钩子将返回一个对象,其中包含从 URL 获取的数据或发生任何错误时的错误。
return { error, data };
最后,向用户指示异步请求的状态通常是一种很好的做法,例如在呈现结果之前显示加载指示器。
因此,让我们在自定义钩子中添加第三个状态变量来跟踪请求的状态。我们将此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 };
};
现在,我们可以将此变量与其他变量一起返回,以便在我们的组件中使用它来在请求运行时呈现加载微调器,以便我们的用户知道我们正在获取他们要求的数据。
return { loading error, data };
在我们了解如何使用新的自定义钩子之前还有一件事。
我们需要检查使用钩子的组件是否仍然挂载,以便更新状态变量。否则,我们的应用程序就会出现内存泄漏。
为此,我们可以简单地创建一个变量来检查组件是否仍然挂载,并在组件卸载时使用 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;
好了!现在,让我们看看使用钩子获取数据有多么简单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>
);
};
使用事件监听器
让我们进入第二个自定义钩子:useEventListener.
这个钩子负责设置和清理我们组件内的事件监听器。
这样,我们每次需要向应用程序添加事件监听器时就不需要重复操作了。
它接受我们想要监听的事件的名称、指定类型的事件发生时运行的函数、监听事件的目标以及事件监听器的一组选项作为参数。
import { useEffect, useRef } from 'react';
const useEventListener = (
eventType = '',
listener = () => null,
target = null,
options = null
) => {};
export default useEventListener;
与上一个钩子一样,我们将使用 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;
然后,我们可以添加实际的事件监听器并在清理函数中将其删除。
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;
实际上,我们还将使用一个引用对象来存储并持久化监听函数,使其在渲染过程中保持持久化。只有当监听函数发生变化时,我们才会更新此引用,并在事件监听方法中使用此引用。
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;
我们不需要从这个钩子返回任何东西,因为我们只是监听事件并运行作为参数传入的处理程序函数。
现在可以轻松地为我们的组件添加事件监听器,例如下面的组件,以检测 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;
使用本地存储
对于我们的第三个自定义钩子,我们将利用localStorage
浏览器的功能来跨会话保持组件的状态。
对于这个,我们需要创建或更新的键的名称localStorage
以及初始值。就是这样!
import { useState } from 'react';
const useLocalStorage = (key = '', initialValue = '') => {};
export default useLocalStorage;
我们将返回一个类似于 React useState
hook 的数组。因此,该数组将包含一个状态值和一个用于在持久化过程中更新它的函数。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;
这里我们使用延迟初始化来读取“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;
此函数同时更新 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;
useMediaQuery
好的!让我们继续讨论第四个 React Hook useMediaQuery
,。
这个钩子将帮助我们在功能组件内部以编程方式测试和监控媒体查询。例如,当你需要根据设备类型或特定特性渲染不同的 UI 时,这非常有用。
所以我们的钩子接受 3 个参数,它们是:
- 首先,对应于媒体查询的字符串数组
- 然后,与这些媒体查询匹配的值数组,其顺序与前一个数组相同
- 最后,如果没有媒体查询匹配,则使用默认值
import { useState, useCallback, useEffect } from 'react';
const useMediaQuery = (queries = [], values = [], defaultValue) => {};
export default useMediaQuery;
在这个钩子中,我们做的第一件事是为每个匹配的媒体查询构建一个媒体查询列表。我们将使用这个数组来匹配媒体查询并获取相应的值。
import { useState, useCallback, useEffect } from 'react';
const useMediaQuery = (queries = [], values = [], defaultValue) => {
const mediaQueryList = queries.map(q => window.matchMedia(q));
};
export default useMediaQuery;
为此,我们在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;
然后,我们创建一个 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;
最后,我们在钩子中添加一个事件监听器,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;
我最近用过一个简单的例子,就是添加一个媒体查询来检查设备是否允许用户将鼠标悬停在元素上。这样,如果用户可以悬停,我就添加特定的不透明度样式;否则,就应用基本样式。
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;
使用暗黑模式
好了,伙计们!还差一个钩子。
这是我最喜欢的。它允许我轻松快速地将暗黑模式功能应用到我的任何 React 应用程序中。
让我们看看如何构建这样的钩子。
此钩子旨在按需启用和禁用暗模式,将当前状态存储在localStorage.
为此,我们将使用刚刚构建的两个钩子:useMediaQuery
和useLocalStorage.
我们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;
最后,这个钩子的最后一部分是触发一个副作用,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;
如果你正在寻找一种简单的方法,那么再次看看支持暗黑模式的 Tailwind CSS。结合这个钩子,Tailwind CSS 成为在任何 React 应用程序中实现暗黑模式最简单、最快捷的方法。
结论
好了!就这样吧,各位。非常感谢你们观看(或阅读)这篇文章。
我真心希望这段视频对你有用。请务必查看Github 仓库,获取我们刚刚构建的所有钩子的源代码。
请与您的朋友分享此视频,点击“赞”按钮,别忘了在 YouTube 上订阅。
成为一名 React 开发者
如果您需要了解有关使用 React 构建现代 Web 应用程序的更多信息,请查看我在 AlterClass.io 上的课程。
我的课程将教您掌握 React、成为一名成功的 React 开发人员并获得聘用所需的一切!
我将教授您使用 React 所需的所有概念,您将通过测验和编程评估获得大量的实践练习,并且您将自己构建真实世界的项目。
此外,您将成为不断壮大的学习者社区的一部分。
因此,请访问 AlterClass.io,注册我的课程,并开始构建强大的 React 应用程序的惊人组合。
文章来源:https://dev.to/alterclass/5-react-custom-hooks-you-should-start-using-explained-5d18