useRef
用法
useRef
主要用于在函数组件中访问和操作 DOM 元素 ,以及保留某些值的引用,以避免它们在组件重新渲染时被重置
创建 ref 对象:使用 useRef 创建一个 ref 对象,可以像这样初始化:initialValue 是可选的,通常用于初始化 ref 的值。
ini
const myRef = useRef(initialValue);
访问 ref 的值:通过 myRef.current
属性访问 ref 的值。这个属性会始终包含对 ref 对象当前值的引用。
ini
const element = myRef.current; // 访问 DOM 元素的引用
案例
通过useRef绑定DOM
ini
// 1.用法一:通过useRef来绑定DOM
const titleRef = useRef();
const inputRef = useRef();
然后在对应的JSX元素处进行绑定
xml
<h2 ref={titleRef}>标题</h2>
<input type="text" ref={inputRef} />
此时就可以编写一些函数来访问和操作DOM,例如下方的打印出DOM信息以及获取文本输入框的焦点
scss
function showTitleDom() {
console.log(titleRef.current);
}
function getInput() {
inputRef.current.focus();
}
通过按钮绑定事件,用于查看标题dom和获取文本框焦点
scss
<button onClick={(e) => showTitleDom()}>查看标题dom</button>
<button onClick={(e) => getInput()}>获取文本框焦点</button>
useImperativeHandle
forwardRef用法
forwardRef将函数组件内部的DOM元素暴露给父组件
forwardRef(render)
,forwardRef接受一个render
函数用于渲染子组件
render函数的形式通常如下:
javascript
forwardRef((props, ref) => {
// 在组件内部使用 ref
// ...
return <div ref={ref}>Hello, World!</div>;
});
因此forwardRef可以创建一个接受 ref
作为参数的子组件。父组件可以通过 ref
来访问子组件内部的 DOM 元素。
可以举一个具体的例子,页面的具体构成如下
这是一个通过forwardRef包裹的子组件Home
javascript
const Home = memo(
forwardRef((props, ref) => {
return (
<div>
<h3>Home组件</h3>
<input type="text" ref={ref} />
</div>
);
})
);
在父组件中,就可以通过将生成的ref实例传递给子组件,以在父组件中访问子组件Home
的DOM元素。因此整个使用过程可以理解为,使用forwardRef将ref转发给子组件,子组件获取到父组件创建的ref,绑定到自己的某个DOM元素上。
ini
const App2 = memo(() => {
const inputRef = useRef();
useEffect(() => {
console.log(inputRef.current);
}, []);
return (
<div>
<h2>App2组件</h2>
<Home ref={inputRef} />
</div>
);
});
useImperativeHandle用法
上一小节提到的forwardRef
可以实现将子组件的DOM元素暴露给父组件,但是有些情况下不希望完全将DOM元素完全暴露给父组件,而仅仅是希望父组件可以执行特定的操作,而不是任意操作DOM。这种场景可以考虑使用useImperativeHandle
useImperativeHandle 允许定义在组件外部可访问的实例值或方法,并将它们暴露给父组件或外部代码。
useImperativeHandle 接受两个参数:
- 第一个参数是 ref 对象
- 第二个参数是一个函数,这个函数返回一个对象,其中包含子组件要暴露给外部的属性和方法。
useImperativeHandle会将传入的ref
和第二个参数返回的对象
进行绑定,因此父组件在使用ref.current时,实际调用的是第二个参数返回的对象。
案例
例如想要定义一个子组件,展示一个文本框。并且想要为父组件提供 文本框聚焦
、以及修改文本框内容
的两个方法
useImperativeHandle
第二个参数传入回调函数,其返回的对象就是对应传入ref的current
javascript
const Home = memo(
forwardRef((props, ref) => {
const textRef = useRef();
useImperativeHandle(ref, () => {
// 暴露给父组件的操作,全放在这个对象里
return {
focus() {
textRef.current.focus();
},
changeContent() {
textRef.current.value = Math.random();
},
};
});
return (
<div>
<h2>Home组件</h2>
<input type="text" ref={textRef} />
</div>
);
})
);
因此可以在父组件中调用上述定义的focus和setValue方法。
为子组件传入inputRef,那么inputRef.current中就包含了focus
方法和changeContent
方法。
javascript
const App3 = memo(() => {
const inputRef = useRef();
console.log(inputRef.current);
function handleFocus() {
inputRef.current.focus();
}
function handleContent() {
inputRef.current.changeContent();
}
return (
<div>
<h2>App组件</h2>
<Home ref={inputRef} />
<button onClick={(e) => handleFocus()}>文本框聚焦</button>
<button onClick={(e) => handleContent()}>修改文本框内容</button>
</div>
);
});
useLayoutEffect
useLayoutEffect 的基本用法与 useEffect 类似,都接受两个参数:一个回调函数和一个依赖数组。当依赖数组中的值发生变化时,useLayoutEffect 的回调函数会被调用。
useLayoutEffect与useEffect类似,区别在于:
- useEffect会在浏览器执行绘制之后异步触发
- useLayoutEffect会在渲染的内容更新到DOM之前执行,阻塞DOM更新
useLayoutEffect意义是可以在 DOM 更新之前执行某些操作,通常用于需要立即获取或计算 DOM 元素属性的情况。可以防止闪烁或不一致的 UI 渲染问题。
案例
当点击按钮时,会将count置为0,在案例中,我们假设count是不可以为0的。因此需要保证count为0时设置为别的数值,这时可以用useLayoutEffect在将count绘制在浏览器之前进行修改。
scss
const App = memo(() => {
const [count, setCount] = useState(10);
// 在count渲染到屏幕前,发现count为0则进行修改
useLayoutEffect(() => {
if (count === 0) {
setCount(Math.random() + 10);
}
}, [count]);
return (
<div>
<h2>count: {count}</h2>
<button onClick={(e) => setCount(0)}>设置count</button>
</div>
);
});
需要注意的是,如果此时考虑使用useEffect进行count修改,则会出现屏幕闪烁问题,即先出现0,再更换为别的数值
scss
useEffect(() => {
if (count === 0) {
setCount(Math.random() + 10);
}
}, [count]);
自定义hook
自定义hook本质是一种封装可重用逻辑的方式。以便不同组件都可以共享该逻辑。
自定义 Hook 的名称通常以 "use
" 开头,在自定义hook内部,可以使用React内置hook,也可以编写自己创建的函数。
抽取Context
假如某业务需要在多个组件都获取同样的Context信息,那么可以考虑将获取context的逻辑封装成一个hook
javascript
import { useContext } from "react";
import { ThemeContext, UserContext } from "../../04_useContext的使用/context";
function useFetchContext() {
const user = useContext(UserContext);
const theme = useContext(ThemeContext);
return [user, theme];
}
export default useFetchContext;
那么在所有组件都可以通过一个hook函数来获取到需要的context信息
监听窗口滚动位置
编写一个自定义hook获取页面滚动的水平和垂直位置,使用这个自定义 Hook,可以在任何组件中获取到页面的滚动位置,而无需在每个组件中重复编写滚动事件的监听逻辑。
scss
import { useEffect, useState } from "react";
function useScrollPosition() {
const [scrollX, setScrollX] = useState(0);
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
function handleScroll() {
// console.log(window.scrollX, "-", window.scrollY);
setScrollX(window.scrollX);
setScrollY(window.scrollY);
}
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return [scrollX, scrollY];
}
export default useScrollPosition;
Redux Hooks
useSelector
useSelector
是 React 函数组件中访问 Redux store 中的状态的一种方式,使用时需要为useSelector传入一个回调函数
,就可以从 Redux store 中选择(select)某些数据进行渲染或其他操作。
基本用法如下:
首先要确保所编写的函数组件位于 Redux Provider 的内部 ,这样它才能访问到 Redux store。因此往往在使用Redux的时候先用Provider
包裹App
组件。
在以下案例中,我们获取到了state状态中counter
子模块的count
属性值。并且注意我的写法,这种方式传入的回调函数,返回值是一个对象,比较适合从redux store中取出多个数据。
(state) => ({ count: state.counter.count,})
javascript
import React, { memo } from "react";
import { useSelector } from "react-redux";
const App = memo((props) => {
// 获取redux store中的数据
const { count } = useSelector((state) => ({
count: state.counter.count,
}));
return (
<div>
<h2>当前计数:{count}</h2>
</div>
);
});
export default App;
useSelector浅层比较
在下列应用场景中,子组件Home中使用useSelector获取到message,并进行渲染,父组件同时渲染count。子组件Home通过memo进行包裹。
javascript
const Home = memo((props) => {
const { message } = useSelector((state) => ({
message: state.counter.message,
}));
console.log("Home render");
return (
<div>
<h2>Home message:{message}</h2>
</div>
);
});
如果更改count,会导致父组件重新渲染,然而Home组件也会被重新渲染。但是Home组件被memo包裹,如果props没更新的话,是应该不会重新渲染的。这是由于useSelector监听state的变化,由于count发生更新,导致state发生变化。即使Home组件中仅仅使用了state中的message,仍然会重新渲染。
因此可以考虑为useSelector传入shallowEqual
,shallowEqual 是 React Redux 提供的浅层比较函数,它会比较对象的属性值,只有当属性值发生实际变化时才认为对象发生了变化。使用 shallowEqual 可以确保只有当 message 属性的内容发生变化时,才会触发 Home 组件的重新渲染,而不受 count 属性的变化影响。
优化之后,更新count的值,仅仅父组件进行重新渲染,子组件Home不会渲染
useDispatch
useDispatch
是 React Redux 提供的 hook,用于在函数组件中获取 Redux store 的 dispatch
函数,从而可以在组件中派发(dispatch)Redux actions。
以下案例就通过useDispatch
获取dispatch函数,并在特定的事件(比如按钮点击)发生时,派发相应的 Redux action。
javascript
import React, { memo } from "react";
import { incrementAction, decrementAction } from "./store/modules/counter";
import { useDispatch, useSelector } from "react-redux";
const App = memo((props) => {
// 获取redux store中的数据
const { count } = useSelector((state) => ({
count: state.counter.count,
}));
const dispatch = useDispatch();
function handleCount(num, isAdd) {
if (isAdd) {
dispatch(incrementAction(num));
} else dispatch(decrementAction(num));
}
return (
<div>
<h2>当前计数:{count}</h2>
<button onClick={(e) => handleCount(1, true)}>+1</button>
<button onClick={(e) => handleCount(10, false)}>-10</button>
</div>
);
});
export default App;
useTransition
用法
官网文档这么描述该hook,useTransition 是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook。
React中,transition
是一个特定的渲染状态,React对处于transition
状态的更新操作
进行优化处理。useTransition
的作用是实现在进行比较耗时的更新状态操作时不阻塞UI的交互。
很多状态更新操作涉及大量计算或数据处理,往往比较耗费时间,会阻塞用户界面。使用useTransition
管理这部分操作后,界面在进行状态更新的同时仍然能够响应用户的交互操作
可以理解为useTransition
可以将部分任务的更新优先级变低,可以稍后进行更新。useTransition
返回一个由两个元素组成的数组:
- isPending:是否存在待处理的 transition(过渡)。如果 isPending 的值为 true,表示当前存在尚未完成的过渡任务
- startTransition 函数,使用 startTransition 函数将
某些状态更新操作
标记为 transition。通过调用 startTransition 函数,React 在处理状态为transition的任务时会进行特殊的优化。
简单的演示案例如下,setData(newData);
是一个非常耗时的更新操作,执行过程中,界面的UI交互会变得很卡顿,因此可以将该部分内容包裹在startTransition
函数中,并且通过isPending
来控制渲染,更新操作未完成时显示Loading组件。
javascript
import React, { useState, useTransition } from 'react';
function MyComponent() {
const [isPending, startTransition] = useTransition();
const [data, setData] = useState(null);
const fetchData = () => {
startTransition(() => {
// 比较耗时的更新操作
setData(newData);
});
};
return (
<div>
<button onClick={fetchData}>Fetch Data</button>
{isPending ? <LoadingSpinner /> : <DataComponent data={data} />}
</div>
);
}
以下是对该hook的进一步说明,以论述useTransition
的作用。
- 标记为 transition 的状态更新将会被其他状态更新打断。例如,在 transition 中更新图表组件,并在图表组件仍在重新渲染时继续在输入框中输入,React 将首先处理输入框的更新,然后再重新启动对图表组件的渲染工作。
- 如果希望某个
状态更新操作
被 useTransition 所追踪,那么应该使用 useState 返回的 setter 函数(通常形如 setSomeState)来进行状态更新。
案例
当前网页渲染10000个公司名的列表,并且在上方有一个输入框,当输入内容时,会查找包含关键词的公司名。例如输入ot
,会显示包含ot的公司名
当删除t
时,由于数据量过大,在删除t时输入框的交互会变的卡顿,因为 渲染新的公司列表
和输入框的交互操作
会同时进行,这时可以考虑采用useTransition
进行优化
将setCompanyNames(filterCompanyNames)
操作放在setTransition
内部,更新公司列表的相关逻辑会标记为transition
状态,此时输入框相关的操作会打断更新公司列表操作。
pending
会标示状态为transition的任务是否完成,如果没完成则会显示Loading效果
javascript
const App = memo(() => {
const [companyNames, setCompanyNames] = useState(arrayData);
const [pending, setTransition] = useTransition();
function valueChangeHandle(event) {
const keyword = event.target.value;
// 将一部分比较复杂的逻辑延后执行
setTransition(() => {
// 筛选出包含关键词的数组项
const filterCompanyNames = arrayData.filter((item) =>
item.includes(keyword)
);
setCompanyNames(filterCompanyNames);
});
}
return (
<div>
<input type="text" onInput={(e) => valueChangeHandle(e)} />
<h2>公司列表 {pending && <span>pending...</span>}</h2>
<ul>
{companyNames.map((item, index) => {
return <li key={index}>{item}</li>;
})}
</ul>
</div>
);
});
export default App;
useDeferredValue
用法
useDeferredValue
提供一种延迟更新 UI 的机制。当有一个新值需要渲染时,React 会在后台异步地进行渲染,确保首先使用旧值进行渲染,避免阻塞用户界面。然后,当渲染完成后,React 再使用新值进行一次渲染,保持 UI 的同步性。
- 参数
value
: 可以将任何类型的值传递给useDeferredValue
。这个值是要在 UI 中延迟更新的数据。 返回值
: 返回值在组件的初始渲染期间与传递的初始值相同。但是在组件更新时,React 会有两次渲染尝试:
-
- 第一次尝试(返回旧值): React 首先尝试使用之前的(旧的)值进行重新渲染,因此在这个阶段 useDeferredValue 返回的值仍然是旧的。
- 第二次尝试(返回更新后的值): 在后台,React 使用你提供的新值进行另一个重新渲染。在这个阶段,useDeferredValue 返回的值将会是更新后的值。
基本用法如下:
javascript
const App = memo(() => {
const [companyNames, setCompanyNames] = useState(arrayData);
// 设置延迟更新的值
const deferCompanyNames = useDeferredValue(companyNames);
return (
<div>
<h2>公司列表 </h2>
<ul>
{deferCompanyNames.map((item, index) => {
return <li key={index}>{item}</li>;
})}
</ul>
</div>
);
});
export default App;
补充:
useDeferredValue
本身不会引起任何固定的延迟。一旦 React 完成原始的重新渲染,它会立即开始使用新的延迟值处理后台重新渲染。
任何由事件(例如用户输入)引起的更新都会中断后台重新渲染,并被优先处理。这确保了用户的输入和交互在应用中的体验是即时的,不会因为延迟而感觉迟钝。
案例
一个页面展示10000条公司列表,在文本框输入关键词,会进行列表更新。由于数量过多,在更改关键词时输入操作会卡顿,因此可以考虑将要渲染的列表数据通过useDeferredValue
包装,那么发生文本框输入事件时,就会中断后台渲染公司列表数据,提升交互体验。
javascript
const App = memo(() => {
const [companyNames, setCompanyNames] = useState(arrayData);
const deferCompanyNames = useDeferredValue(companyNames);
function valueChangeHandle(event) {
const keyword = event.target.value;
// 筛选出包含关键词的数组项
const filterCompanyNames = arrayData.filter((item) =>
item.includes(keyword)
);
setCompanyNames(filterCompanyNames);
}
return (
<div>
<input type="text" onInput={(e) => valueChangeHandle(e)} />
<h2>公司列表 </h2>
<ul>
{deferCompanyNames.map((item, index) => {
return <li key={index}>{item}</li>;
})}
</ul>
</div>
);
});