当前位置: 首页 > article >正文

React的hook✅

为什么hook必须在组件内的顶层声明?

这是为了确保每次组件渲染时,Hooks 的调用顺序保持一致。React利用 hook 的调用顺序来跟踪各个 hook 的状态。每当一个函数组件被渲染时,所有的 hook 调用都是按照从上到下的顺序依次执行的。React 内部会维护一个状态列表,这个列表中的每个状态项都对应一个 hook 的调用,包括 useState、useEffect 等。当你调用 useState(initialValue) 时,React 会在内部为这个状态分配一个索引。该索引基于 hook 调用的顺序。例如,第一次调用 useState 时,它会在状态列表的第一个位置存储状态,第二次调用会在第二个位置存储,以此类推

考虑下面这个demo:

const Component = () => {
	const [count, setCount] = useState(0);
	const handleOfClick = () => setCount(count + 1);
	return <button onClick={setCount}>{count}</button>
}

Q:既然每次视图的更新都会重新执行整个函数,那必然会执行到const [count, setCount] = useState(0)这句代码。如果我在上一次更新中把count加到10,为什么在新的渲染周期中,React能记住这个10而不是传给useState的0呢?
A:当组件重新渲染时,React 会根据组件的调用顺序再次按顺序调用对应的 hook。这样,React 可以确保它能够始终访问到正确的状态。例如,当第二次渲染时,React 知道第一个 useState 是哪个状态,因为它在第一次渲染时已经分配了这个状态的索引,这个索引是靠hook调用的顺序产生的索引来追踪的。 所以如果在条件语句、循环或嵌套函数中调用 hook,可能会导致调用顺序的变化,从而产生不可预知的状态。


useImperativeHandle

useImperativeHandle通常是和forwardRef配合使用的,用来把子组件中的属性或者方法暴露给父组件,在进行组件的封装或者组件间的通信的时候常会使用。如下:

// 封装一个可拖拽的组件
const DragComponent = forwardRef(
	(
		props: {
			children: React.ReactNode, // 求求你不要挂一个很复杂的组件进来🙏
			// other config...
		},
		ref // ref是必须的
	) => {
	
		// 复位
		const resetPosition = () => {
			// todo: 可以在父组件中调用,让这个可拖拽的组件在父组件中回到第一次渲染的位置
		}
		useImperativeHandle(ref, () => {
			resetPosition // 显式声明
		})
		
		return (
			<div>{props.children}</div>
		)
	}
)

// 之后在某一个页面中使用它
const Page = () => {
	const dragRef = useRef(null)
	const handleOfClick = () => {
		dragRef.current?.reset();
	}
	
	return (
		<div>
			<DragComponent ref={dragRef}/>
			<button onClick={handleOfClick}>复位</button>
		</div>
	)
}

useCallback

useCallBack用来缓存一个函数的引用,它常常配合memo使用以提高渲染的性能。

const Component = memo((
	{ count, setCount }: { count: number; setCount: () => void }
) => {
	console.log("CountComponeng render");
    return (
      <div>
        <button onClick={() => setCount()}>CountComponeng: {count}</button>
      </div>
    );

})

function App() {
	console.log("App render");
	const [parentCount, setParentCount] = useState(0);
	const [childCount, setChildCount] = useState(0);

	const addChildCount = useCallback(() => {
 		setChildCount(childCount + 1);
  	}, [childCount]);
  	
	// 这样也行
	// const addChildCount = useCallback(() => {
 	//     setChildCount((count) => count + 1);
  	// }, []);
	

  return (
    <div id="app">
      <h1>Hello Vite + React!</h1>
      <button onClick={() => setParentCount(parentCount + 1)}>
        parentCount: {parentCount}
      </button>
      <CountComponeng setCount={setChildCount} count={childCount} />
    </div>
  );
}

export default App;


useReducer

类似redux的更规范的写法,我用的还不多😢,权当记录(该说不说,确实优雅):

import React, { useReducer } from "react";

// 定义初始状态
const initialState = { count: 0 };

// 定义 reducer 函数
const reducer = (state, action) => {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
    default:
      return state;
  }
};

const Counter = () => {
  // 使用 useReducer
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "increment" })}>Increment</button>
      <button onClick={() => dispatch({ type: "decrement" })}>Decrement</button>
    </div>
  );
};
export default Counter;

setState的函数写法和变量写法

我有这样的代码:const [count, setCount] = useState(0),在普通的情况下setCount(count + 1)setCount((count) => count + 1)都能实现count加1并更新视图的操作。

但是,考虑下面这个demo:

const handleOfClick = () => {
	setCount(count+1)
	setCount(count+1)
	setCount(count+1)
}

每一次点击,count最终都只能加1而不能加3,这个React官网介绍的很清楚这里不多说。但是如果把上面的setCount(count+1)换成setCount((count) => count + 1),确实能实现点击一次就+3并更新视图的功能,因为这种函数的写法保证了在进行状态更新时,能够获取到最新的状态值,特别是在状态更新依赖于之前的状态值时,可以避免因为异步执行导致的潜在问题。
始终记住setState是异步的,而且不是没setState一次就更新一次视图的(涉及到React为了优化渲染性能而使用的批量更新策略)。

再考虑一个更普遍的场景:

const Page = () => {
	const [count, setCount] = useState(1);
	useEffect(() => {
		const scrollableContainer = document.getElementById("scrollable-container");
		scrollableContainer?.addEventListener("scroll", () => {
      		setCount(count + 1)
    	});
	}, [])
	return <div id="scroll-container">{count}</div>
}

你会发现,任凭你滚动的再快,count只会加到2,然后就一直不变了。因为此处的useEffect只会执行一次,当你使用 addEventListener 直接绑定事件时,你得到的是一个闭包。在这个闭包中,count 的值是在事件绑定时捕获的(1)
但是把setCount(count+1)换成setCount((count) => count + 1)就能每次滚动的时候都加1,因为它会接受当前状态作为参数,这样每次更新都会基于最新的状态进行计算,从而避免因为闭包问题导致的状态不正确的问题。


http://www.kler.cn/a/405003.html

相关文章:

  • .net6 使用 FreeSpire.XLS 实现 excel 转 pdf - docker 部署
  • gitlab:使用脚本批量下载项目,实现全项目检索
  • 6. Spring Cloud Gateway网关超详细内容配置解析说明
  • json-bigint处理前端精度丢失问题
  • 招商蛇口|在低密园林里,开启生活的“任意门”
  • 7天掌握SQL - 第三天:MySQL实践与索引优化
  • CSV文件数据导入hive
  • 开发中使用UML的流程_02 CIM-1:定义业务流程
  • Docker 安装单机版mysql 并持久化数据
  • 【GNU】addr2line
  • 大前端的发展过程
  • 图像处理 之 凸包和最小外围轮廓生成
  • 开发体育赛事直播平台防止数据泄露的技术安全方案
  • Redis性能优化的18招
  • 掌握Golang中的数据竞争检测:runtime/race包全面教程
  • 探索Linux内核中的Runqueue:从O(n)到O(1)的演进与负载均衡应用
  • 卷积神经网络(CNN)中的权重(weights)和偏置项(bias)
  • qt连接postgres数据库时 setConnectOptions函数用法
  • Docker部署Canal实现将Mysql数据同步至ES
  • 机器学习笔记——KNN(K-Nearest Neighbors,K 近邻算法)
  • 【MySQL的故事】认识MySQL中的聚合函数以及聚合函数的作用,拿捏这些细节
  • Idea集成ApiFox插件
  • Percona XtraBackup备份docker版本mysql 5.7
  • 趋势洞察|AI 能否带动裸金属 K8s 强势崛起?
  • 什么是反向 DNS 查找以及它的作用是什么?
  • Banana Pi BPI-CanMV-K230D-Zero 采用嘉楠科技 K230D RISC-V芯片设计