ReactJS - 自定义钩子



钩子是功能组件的组成部分。它们可用于增强功能组件的特性。React 提供了很少的内置钩子。尽管内置钩子功能强大并且可以执行任何功能,但专用钩子是必需的,并且需求量很大。

React 理解开发者的这个需求,并允许通过现有的钩子创建新的自定义钩子。开发者可以从函数组件中提取特殊功能,并将其创建为单独的钩子,可以在任何函数组件中使用。

在本章中,让我们学习如何创建自定义钩子。

创建自定义钩子

让我们使用创建一个具有无限滚动特性的新 react 函数组件,然后从函数组件中提取无限功能并创建一个自定义钩子。一旦创建了自定义钩子,我们将尝试更改原始函数组件以使用我们的自定义钩子。

实现无限滚动功能

该组件的基本功能是仅通过生成待办事项来显示待办事项的虚拟列表。当用户滚动时,该组件将生成一组新的虚拟待办事项列表,并将其附加到现有列表中。

首先,创建一个新的 react 应用程序并使用以下命令启动它。

create-react-app myapp
cd myapp
npm start

接下来,在组件文件夹 (src/components/TodoList.js) 下创建一个 react 组件 TodoList


function TodoList() {
	 	return <div>Todo List</div>
}
export default TodoList

接下来,更新根组件,App.js使用新创建的 TodoList 组件。


import TodoList from './components/TodoList';
function App() {
	 	return (
	 	 	 <div style={{ padding: "5px"}}>
	 	 	 	 	<TodoList />
	 	 	 </div>
	 	);
}
export default App;

接下来,创建一个计数状态变量,以保持要生成的待办事项的数量。


 const [count, setCount] = useState(100)

这里

  • 要生成的初始项目数为 100
  • count 是用于维护待办事项数量的状态变量

接下来,创建一个数据数组来维护虚拟生成的待办事项,并使用 useMemo 钩子保留引用。


React.useMemo(() => {
	 	for (let i = 1; i <= count; i++) {
	 	 	 data.push({
	 	 	 	 	id: i,
	 	 	 	 	todo: "Todo Item " + i.toString()
	 	 	 })
	 	}
}, [count]);

这里

  • 使用 useMemo 来限制每次渲染时的待办事项的生成。
  • 使用 count 作为依赖项,在计数更改时重新生成待办事项

接下来,将处理程序附加到滚动事件,并将待办事项的计数更新为在用户移动到页面末尾时生成的计数。


React.useEffect(() => {
	 	function handleScroll() {
	 	 	 const isBottom =
	 	 	 	 	window.innerHeight + document.documentElement.scrollTop
	 	 	 	 	=== document.documentElement.offsetHeight;
	 	 	 if (isBottom) {
	 	 	 	 	setCount(count + 100)
	 	 	 }
	 	}
	 	window.addEventListener("scroll", handleScroll);
	 	return () => {
	 	 	 window.removeEventListener("scroll", handleScroll);
	 	};
}, [])

在这里,我们有,

  • 使用 useEffect 钩子来确保 DOM 已准备好附加事件。
  • 附加了一个事件处理程序,scrollHandler 用于滚动事件。
  • 从应用程序中卸载组件时删除了事件处理程序
  • 发现用户在滚动事件处理程序中使用 DOM 属性点击页面底部。
  • 一旦用户点击页面底部,计数就会增加 100
  • 一旦更改了计数状态变量,组件就会被重新渲染,并加载更多的待办事项。

最后,渲染待办事项,如下所示 -


<div>
	 	<p>List of Todo list</p>
	 	<ul>
	 	 	 {data && data.map((item) =>
	 	 	 	 	<li key={item.id}>{item.todo}</li>
	 	 	 )}
	 	</ul>
</div>

该组件的完整源代码如下:


import React, { useState } from "react"
function TodoList() {
	 	const [count, setCount] = useState(100)
	 	let data = []
	 	
	 	React.useMemo(() => {
	 	 	 for (let i = 1; i <= count; i++) {
	 	 	 	 	data.push({
	 	 	 	 	 	 id: i,
	 	 	 	 	 	 todo: "Todo Item " + i.toString()
	 	 	 	 	})
	 	 	 }
	 	}, [count]);
	 	
	 	React.useEffect(() => {
	 	 	 function handleScroll() {
	 	 	 	 	const isBottom =
	 	 	 	 	 	 window.innerHeight + document.documentElement.scrollTop
	 	 	 	 	 	 === document.documentElement.offsetHeight;
	 	 	 	 	
	 	 	 	 	if (isBottom) {
	 	 	 	 	 	 setCount(count + 100)
	 	 	 	 	}
	 	 	 }
	 	 	 window.addEventListener("scroll", handleScroll);
	 	 	 return () => {
	 	 	 	 	window.removeEventListener("scroll", handleScroll);
	 	 	 };
	 	}, [])
	 	return (
	 	 	 <div>
	 	 	 	 	<p>List of Todo list</p>
	 	 	 	 	<ul>
	 	 	 	 	 	 {data && data.map((item) =>
	 	 	 	 	 	 	 	<li key={item.id}>{item.todo}</li>
	 	 	 	 	 	 )}
	 	 	 	 	</ul>
	 	 	 </div>
	 	)
}
export default TodoList

接下来,打开浏览器并运行应用程序。一旦用户点击页面末尾,应用程序将附加新的待办事项,并无限地继续进行,如下所示 -

实现无限滚动功能

实现 useInfiniteScroll 钩子

接下来,让我们尝试通过从现有组件中提取逻辑来创建新的自定义钩子,然后在单独的组件中使用它。创建一个新的自定义钩子,useInfiniteScroll (src/Hooks/useInfiniteScroll.js),如下所示


import React from "react";
export default function useInfiniteScroll(loadDataFn) {
	 	const [bottom, setBottom] = React.useState(false);
	 	React.useEffect(() => {
	 	 	 window.addEventListener("scroll", handleScroll);
	 	 	 return () => {
	 	 	 	 	window.removeEventListener("scroll", handleScroll);
	 	 	 };
	 	}, []);
	 	React.useEffect(() => {
	 	 	 if (!bottom) return;
	 	 	 loadDataFn();
	 	}, [bottom]);
	 	
	 	function handleScroll() {
	 	 	 if (window.innerHeight + document.documentElement.scrollTop
	 	 	 	 	!= document.documentElement.offsetHeight) {
	 	 	 	 	return;
	 	 	 }
	 	 	 setBottom(true)
	 	}
	 	return [bottom, setBottom];
}

在这里,我们有,

  • 使用 use 关键字来命名自定义钩子。这是使用 use 关键字开始自定义钩子名称的一般做法,这也是让反应者知道函数是自定义钩子的提示
  • 使用了状态变量,bottom来了解用户是否点击了页面的底部。
  • 使用 useEffect 在 DOM 可用时注册滚动事件
  • 在组件中创建钩子期间使用了泛型函数 loadDataFn。这将允许创建用于加载数据的自定义逻辑。
  • 在滚动事件处理程序中使用 DOM 属性来跟踪用户滚动位置。当用户点击页面底部时,底部状态变量会发生变化。
  • 返回底部状态变量 (bottom) 的当前值和更新底部状态变量 (setBottom 的函数))

接下来,创建一个新组件 TodoListUsingCustomHook 以应用新创建的钩子。


function TodoListUsingCustomHook() {
	 	return <div>Todo List</div>
}
export default TodoListUsingCustomHook

接下来,更新根组件,App.js使用新创建的 TodoListUsingCustomHook 组件。


import TodoListUsingCustomHook from './components/TodoListUsingCustomHook';
function App() {
	 	return (
	 	 	 <div style={{ padding: "5px"}}>
	 	 	 	 	<TodoListUsingCustomHook />
	 	 	 </div>
	 	);
}
export default App;

接下来,创建一个状态变量,用于管理待办事项列表项的数据。


 const [data, setData] = useState([])

接下来,创建一个状态变量 count 来管理要生成的待办事项列表项的数量。


 const [data, setData] = useState([])

接下来,应用无限滚动自定义钩子。


 const [bottom, setBottom] = useInfiniteScroll(loadMoreData)

这里,bottom 和 setBottom 由 useInfiniteScroll 钩子公开。

接下来,创建用于生成待办事项列表项的函数 loadMoreData


function loadMoreData() {
	 	let data = []
	 	for (let i = 1; i <= count; i++) {
	 	 	 data.push({
	 	 	 	 	id: i,
	 	 	 	 	todo: "Todo Item " + i.toString()
	 	 	 })
	 	}
	 	setData(data)
	 	setCount(count + 100)
	 	setBottom(false)
}

这里

  • 根据计数状态变量生成待办事项
  • 将计数状态变量递增 100
  • 使用 setData 函数将生成的待办事项列表设置为数据状态变量
  • 使用 setBottom 函数将底部状态变量设置为 false

接下来,生成初始待办事项,如下所示 -


const loadData = () => {
	 	let data = []
	 	for (let i = 1; i <= count; i++) {
	 	 	 data.push({
	 	 	 	 	id: i,
	 	 	 	 	todo: "Todo Item " + i.toString()
	 	 	 })
	 	}
	 	setData(data)
}
React.useEffect(() => {
	 	loadData(count)
}, [])

这里

  • 初始待办事项是根据初始计数状态变量生成的
  • 使用 setData 函数将生成的待办事项列表设置为数据状态变量
  • 使用 useEffect 确保在 DOM 可用时生成数据

最后,渲染待办事项,如下所示 -


<div>
	 	<p>List of Todo list</p>
	 	<ul>
	 	 	 {data && data.map((item) =>
	 	 	 	 	<li key={item.id}>{item.todo}</li>
	 	 	 )}
	 	</ul>
</div>

该组件的完整源代码如下图所示 -


import React, { useState } from "react"
import useInfiniteScroll from '../Hooks/useInfiniteScroll'

function TodoListUsingCustomHook() {
	 	const [data, setData] = useState([])
	 	const [count, setCount] = useState(100)
	 	const [bottom, setBottom] = useInfiniteScroll(loadMoreData)
	 	
	 	const loadData = () => {
	 	 	 let data = []
	 	 	 for (let i = 1; i <= count; i++) {
	 	 	 	 	data.push({
	 	 	 	 	 	 id: i,
	 	 	 	 	 	 todo: "Todo Item " + i.toString()
	 	 	 	 	})
	 	 	 }
	 	 	 setData(data)
	 	}
		
	 function loadMoreData() {
	 	 	 let data = []
	 	 		
	 	 	 for (let i = 1; i <= count; i++) {
	 	 	 	 	data.push({
	 	 	 	 	 	 id: i,
	 	 	 	 	 	 todo: "Todo Item " + i.toString()
	 	 	 	 	})
	 	 	 }
	 	 	 setData(data)
	 	 	 setCount(count + 100)
	 	 	 setBottom(false)
	 	}
	 	React.useEffect(() => {
	 	 	 loadData(count)
	 	}, [])
	 	return (
	 	 	 <div>
	 	 	 	 	<p>List of Todo list</p>
	 	 	 	 	<ul>
	 	 	 	 	 	 {data && data.map((item) =>
	 	 	 	 	 	 	 	<li key={item.id}>{item.todo}</li>
	 	 	 	 	 	 )}
	 	 	 	 	</ul>
	 	 	 </div>
	 	)
}
export default TodoListUsingCustomHook

最后,打开浏览器并检查输出。一旦用户点击页面末尾,应用程序将附加新的待办事项,并无限地继续进行,如下所示 -

实现 UseInfiniteScroll 钩子

行为与 TodoList 组件的行为相同。我们已经成功地从组件中提取了逻辑,并使用它来创建我们的第一个自定义钩子。现在,自定义钩子可以在任何应用程序中使用。

总结

自定义钩子是一种现有的特性,用于将可重用逻辑与函数组件分离,并允许使其真正可重用的钩子跨不同的函数组件。