数据驱动更新 State Hook
useState
useState 是 React 中的一个 Hook,用于在函数组件中添加状态。它接受一个初始状态作为参数,并返回一个包含当前状态值和更新状态值的数组。
参数说明:
初始状态:可以是任何 JavaScript 数据类型,用来初始化状态的值。
返回说明:
数组:包含当前状态值和更新状态值的数组,第一个元素是当前状态值,第二个元素是更新状态值的函数。
应用场景:
在函数组件中添加和管理状态。
替代类组件中的 this.state 和 this.setState。
用法举例:
import React, { useState } from 'react';
const ExampleComponent: React.FC = () => {
const [count, setCount] = useState<number>(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
export default ExampleComponent;
在上面的例子中,我们使用 useState 创建了一个名为 count 的状态变量,并初始化为 0。我们还定义了一个 increment 函数,用于增加 count 的值。最后,我们在 JSX 中展示了当前 count 的值,并在按钮点击事件中调用 increment 函数来更新 count 的值。
useReducer
useReducer 是 React 中的另一个 Hook,用于管理复杂的状态逻辑。它接受一个 reducer 函数和初始状态作为参数,并返回当前状态值和 dispatch 函数。
参数说明:
reducer 函数:接受两个参数,当前状态和 action,返回新的状态。
初始状态:可以是任何 JavaScript 数据类型,用来初始化状态的值。
返回说明:
数组:包含当前状态值和 dispatch 函数的数组,第一个元素是当前状态值,第二个元素是派发 action 的函数。
应用场景:
状态逻辑较复杂,需要根据不同 action 进行状态更新的情况。
替代 useState,在某些情况下更方便和易于维护。
用法举例:
import React, { useReducer } from 'react';
type State = {
count: number;
};
type Action = {
type: 'increment' | 'decrement';
};
const initialState: State = {
count: 0,
};
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
return state;
}
};
const ExampleComponent: React.FC = () => {
const [state, dispatch] = useReducer(reducer, initialState);
const increment = () => {
dispatch({ type: 'increment' });
};
const decrement = () => {
dispatch({ type: 'decrement' });
};
return (
<div>
<p>Count: {state.count}</p>
<button onClick={increment}>Increment</button>
<button onClick={decrement}>Decrement</button>
</div>
);
};
export default ExampleComponent;
在上面的例子中,我们使用 useReducer 创建了一个名为 count 的状态变量,并初始化为 0。我们定义了一个 reducer 函数来处理不同的 action 类型并更新状态。然后,我们使用 dispatch 函数来派发不同的 action,从而更新状态。最后,在 JSX 中展示当前 count 的值,并在按钮点击事件中调用对应的 dispatch 函数来更新 count 的值。
状态传递 Context Hook
useContext
useContext 是 React 中的一个 Hook,用于在函数组件中访问 Context。它接受一个 Context 对象作为参数,并返回该 Context 的当前值。
参数说明:
Context 对象:通过 React.createContext 创建的 Context 对象。
返回说明:
任意类型:返回该 Context 的当前值。
应用场景:
在函数组件中访问全局的状态或其他共享数据。
避免在组件树中层层传递 props。
用法举例:
import React, { createContext, useContext } from 'react';
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
const App: React.FC = () => {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
};
const Toolbar: React.FC = () => {
const theme = useContext(ThemeContext);
return (
<div>
<p>Current Theme: {theme}</p>
</div>
);
};
export default App;
在上面的例子中,我们创建了一个 ThemeContext,其默认值为 'light'。在 App 组件中,我们使用 ThemeContext.Provider 来提供当前主题值为 'dark',并渲染了 Toolbar 组件。在 Toolbar 组件中,我们使用 useContext(ThemeContext) 来获取当前主题值,并在 JSX 中展示出来。这样就实现了在函数组件中访问全局的主题值而不需要通过 props 层层传递。
元素获取 Ref Hook
useRef
useRef
是 React 中的一个 Hook,主要用于访问和存储对 DOM 元素的引用或保存不需要触发重新渲染的可变值。
参数说明:
initialValue: 可选,初始值。可以是任何类型,通常用于存储一个对象或 DOM 引用。
返回说明:
返回一个可变的 ref 对象,ref 对象的 .current 属性可以用来存储任何值。
应用场景:
访问 DOM 元素: 通过 useRef 可以直接访问组件中的 DOM 元素,常用于获取焦点、选择文本等。
存储可变值: 在函数组件中,useRef 可以用来存储不需要重新渲染的可变值,例如计时器 ID、先前的状态等。
保持引用: useRef 可以保持对某个值的引用,而不会在组件重新渲染时重置该值。
用法举例:
import React, { useRef } from 'react';
const ExampleComponent: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus(); // 使用可选链安全地访问 current
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus the input</button>
</div>
);
};
export default ExampleComponent;
在上面的例子中,我们使用 useRef
创建了一个 inputRef
,它将用于引用输入框元素。我们通过 ref
属性将 inputRef
关联到输入框上。当用户点击按钮时,focusInput
函数会调用 inputRef.current?.focus()
,使输入框获得焦点。
import React, { useRef, useEffect } from 'react';
const TimerComponent: React.FC = () => {
const timerRef = useRef<NodeJS.Timeout | null>(null);
const countRef = useRef(0);
useEffect(() => {
timerRef.current = setInterval(() => {
countRef.current += 1;
console.log(countRef.current);
}, 1000);
// 清理定时器
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []);
return <div>Check the console for count updates every second.</div>;
};
export default TimerComponent;
在这个例子中,我们使用 useRef
来存储定时器的引用和计数器的值。countRef
用于记录计数器的当前值,而不会导致组件重新渲染。定时器每秒增加计数器的值并输出到控制台。组件卸载时,定时器会被清理。
useImperativeHandle
useImperativeHandle
是 React 中的一个 Hook,通常与 forwardRef
一起使用,用于自定义组件暴露给父组件的实例值。它允许你在函数组件中控制通过 ref
传递给父组件的值。
参数说明:
ref
: 传递给forwardRef
的 ref 对象。createHandle
: 一个函数,返回一个对象,该对象包含要暴露给父组件的实例值。
返回说明:
void
: 该 Hook 不返回任何值。
应用场景:
自定义组件的实例方法: 当你需要在父组件中调用子组件的方法时,可以使用
useImperativeHandle
来暴露这些方法。控制子组件的行为: 允许父组件通过 ref 直接控制子组件的某些行为。
用法举例:
import React, { useImperativeHandle, forwardRef, useRef, useState } from 'react';
type ChildRef = {
focus: () => void;
};
const ChildComponent = forwardRef<ChildRef, {}>((_, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus(); // 暴露 focus 方法
},
}));
return <input ref={inputRef} type="text" />;
});
const ParentComponent: React.FC = () => {
const childRef = useRef<ChildRef>(null);
const handleFocus = () => {
childRef.current?.focus(); // 调用子组件的 focus 方法
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleFocus}>Focus the input in Child</button>
</div>
);
};
export default ParentComponent;
在上面的例子中,我们创建了一个 ChildComponent
,它使用 forwardRef
来接收父组件传递的 ref
。在 ChildComponent
中,我们使用 useImperativeHandle
来暴露一个 focus
方法,该方法可以使输入框获得焦点。
在 ParentComponent
中,我们创建了一个 childRef
,并将其传递给 ChildComponent
。当用户点击按钮时,handleFocus
函数会调用 childRef.current?.focus()
,从而触发子组件的 focus
方法,使输入框获得焦点。
总结:useImperativeHandle
使得父组件能够控制子组件的行为,提供了一种灵活的方式来管理组件间的交互。
副作用 Effect Hook
useEffect
useEffect
是 React 中的一个 Hook,用于在函数组件中执行副作用操作。它接受一个函数和一个依赖数组作为参数,并在每次渲染后执行该函数。
参数说明:
effect
: 一个函数,用于执行副作用操作。deps
: 一个数组,包含影响副作用操作的依赖项。当依赖项发生变化时,effect
函数会被重新执行。
返回说明:
void
: 该 Hook 不返回任何值。
应用场景:
数据获取: 在组件挂载后获取数据。
订阅事件: 在组件挂载后订阅事件,组件卸载前取消订阅。
手动操作 DOM: 在组件挂载后或更新后手动操作 DOM 元素。
清理副作用: 在组件卸载前清理副作用。
用法举例:
import React, { useEffect, useState } from 'react';
const ExampleComponent: React.FC = () => {
const [data, setData] = useState<string>('');
// 模拟数据获取
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result.data);
};
fetchData();
return () => {
// 在组件卸载前清理副作用
console.log('Cleaning up');
};
}, []); // 空依赖数组表示只在组件挂载和卸载时执行
return (
<div>
<p>Data: {data}</p>
</div>
);
};
export default ExampleComponent;
在这个示例中,我们使用 useEffect
在组件挂载后获取数据,并在组件卸载前清理副作用。在 useEffect
的回调函数中,我们定义了一个 fetchData
函数来获取数据,并在组件挂载后调用它。同时,我们返回一个清理函数,用于在组件卸载前执行清理操作。在依赖数组中传入空数组表示 useEffect
只在组件挂载和卸载时执行。
这种方式可以帮助我们管理组件中的副作用操作,确保在适当的时机执行相应的操作,并在组件卸载时清理副作用,避免内存泄漏和其他问题。
useLayoutEffect
useLayoutEffect
是 React 中的一个 Hook,与 useEffect
类似,用于在函数组件中执行副作用操作。不同之处在于 useLayoutEffect
会在 DOM 变更之后同步触发,而 useEffect
则是在浏览器绘制完成后异步触发。
参数说明:
effect
: 一个函数,用于执行副作用操作。deps
: 一个数组,包含影响副作用操作的依赖项。当依赖项发生变化时,effect
函数会被重新执行。
返回说明:
void
: 该 Hook 不返回任何值。
应用场景:
DOM 操作: 在 DOM 变更之后立即执行操作,确保操作不会影响布局。
测量元素尺寸: 在 DOM 变更后立即测量元素的尺寸或位置。
动画控制: 在 DOM 变更后立即控制动画效果。
同步更新布局: 需要在 DOM 变更后立即更新布局的情况。
用法举例:
import React, { useLayoutEffect, useState, useRef } from 'react';
const ExampleComponent: React.FC = () => {
const [width, setWidth] = useState<number>(0);
const elementRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const handleResize = () => {
if (elementRef.current) {
const { width } = elementRef.current.getBoundingClientRect();
setWidth(width);
}
};
window.addEventListener('resize', handleResize);
handleResize(); // 立即执行一次
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空依赖数组表示只在组件挂载和卸载时执行
return (
<div ref={elementRef}>
<p>Width: {width}px</p>
</div>
);
};
export default ExampleComponent;
在这个示例中,我们使用 useLayoutEffect
来在 DOM 变更之后立即测量元素的宽度,并在窗口大小变化时实时更新宽度。在 useLayoutEffect
的回调函数中,我们定义了一个 handleResize
函数,用于获取元素的宽度并更新状态。然后我们通过 window.addEventListener
监听窗口大小变化事件,并在组件挂载时立即执行一次 handleResize
。在依赖数组中传入空数组表示 useLayoutEffect
只在组件挂载和卸载时执行。
通过使用 useLayoutEffect
,我们可以在 DOM 变更后立即执行操作,确保操作不会影响布局,适用于需要同步更新布局或执行 DOM 操作的场景。
useInsertionEffect
useInsertionEffect
是 React 18 中引入的一个 Hook,主要用于在 DOM 更新之前插入样式或执行其他副作用。它的执行时机比 useLayoutEffect
更早,适用于需要在浏览器绘制之前进行样式插入的场景。
参数说明:
effect
: 一个函数,用于执行副作用操作。deps
: 一个数组,包含影响副作用操作的依赖项。当依赖项发生变化时,effect
函数会被重新执行。
返回说明:
void
: 该 Hook 不返回任何值。
应用场景:
动态样式插入: 在组件渲染之前插入动态生成的样式,确保样式在浏览器绘制之前生效。
CSS-in-JS 库: 在使用 CSS-in-JS 库时,确保样式在 DOM 更新之前插入。
避免闪烁: 在需要避免样式闪烁的情况下,使用
useInsertionEffect
可以确保样式在渲染之前就已经应用。
用法举例:
import React, { useInsertionEffect, useState } from 'react';
const ExampleComponent: React.FC = () => {
const [color, setColor] = useState<string>('blue');
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = `
.dynamic {
color: ${color};
}
`;
document.head.appendChild(style);
return () => {
document.head.removeChild(style); // 清理样式
};
}, [color]); // 当 color 变化时重新插入样式
return (
<div>
<p className="dynamic">This text will change color.</p>
<button onClick={() => setColor(color === 'blue' ? 'red' : 'blue')}>
Toggle Color
</button>
</div>
);
};
export default ExampleComponent;
在这个示例中,我们使用 useInsertionEffect
来动态插入样式。每当 color
状态变化时,我们都会创建一个新的 <style>
元素,并将其插入到文档的 <head>
中。这样,文本的颜色会根据按钮的点击而变化。
通过使用 useInsertionEffect
,我们确保样式在浏览器绘制之前就已经插入,从而避免了可能的闪烁或样式不一致的问题。清理函数会在组件卸载或依赖项变化时移除插入的样式,确保不会造成内存泄漏。
useInsertionEffect
和 useEffect
的区别
执行时机:
useInsertionEffect
在 DOM 变更之前同步执行,这意味着它会在浏览器绘制之前运行,适合需要插入样式或进行布局计算的场景。useEffect
在 DOM 变更后异步执行,适合大多数副作用操作,如数据获取、事件监听等。
适用场景:
useInsertionEffect
主要用于需要确保样式或其他操作在用户看到变化之前完成的场景。useEffect
则更为通用,可以处理各种副作用,且通常是开发者最常用的选择。
总结
虽然在某些情况下 useInsertionEffect
和 useEffect
可以实现相同的功能,但它们的执行时机和适用场景有所不同。useInsertionEffect
是用于特定场景的优化工具,通常在处理样式和布局时使用,而 useEffect
则是处理一般副作用的主要手段。选择使用哪一个 Hook 取决于你的具体需求。
性能 Hook
useMemo
useMemo
是 React 中的一个 Hook,用于对计算昂贵的值进行缓存,避免在每次渲染时重新计算。它接受一个函数和依赖数组作为参数,并返回计算结果。
参数说明:
factory
: 一个函数,用于计算需要缓存的值。deps
: 一个数组,包含影响计算结果的依赖项。当依赖项发生变化时,将重新计算值。
返回说明:
T
: 计算结果的类型,根据传入的factory
函数的返回类型确定。
应用场景:
性能优化: 缓存昂贵的计算结果,避免在每次渲染时重新计算。
避免不必要的重渲染: 通过缓存值,可以避免不必要的组件重渲染。
优化组件性能: 在计算量大的场景下,使用
useMemo
可以提高组件的性能。
用法举例:
import React, { useMemo, useState } from 'react';
const ExampleComponent: React.FC = () => {
const [count, setCount] = useState<number>(0);
// 计算平方值
const squaredValue = useMemo(() => {
console.log('Calculating squared value');
return count ** 2;
}, [count]); // 当 count 变化时重新计算
return (
<div>
<p>Count: {count}</p>
<p>Squared Value: {squaredValue}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
</div>
);
};
export default ExampleComponent;
在这个示例中,我们使用 useMemo
缓存了计算的平方值。每当 count
状态变化时,squaredValue
会重新计算,但在同一个 count
值下,不会重复计算平方值。这可以减少不必要的计算,提高性能。
通过使用 useMemo
,我们可以避免在每次渲染时都重新计算昂贵的值,优化组件性能,并确保只在必要时才进行计算。
useCallback
useCallback
是 React 中的一个 Hook,用于缓存回调函数,避免在每次渲染时重新创建新的回调函数。它接受一个回调函数和依赖数组作为参数,并返回一个 memoized(记忆化)版本的回调函数。
参数说明:
callback
: 一个回调函数,需要被缓存。deps
: 一个数组,包含影响回调函数的依赖项。当依赖项发生变化时,将重新创建新的回调函数。
返回说明:
T
: 缓存的回调函数的类型,根据传入的callback
函数的类型确定。
应用场景:
避免不必要的子组件重渲染: 缓存回调函数可以避免子组件在每次渲染时重新创建新的回调函数,提高性能。
传递给子组件的回调函数: 在将回调函数传递给子组件时,使用
useCallback
可以确保子组件不会因为父组件的重新渲染而触发不必要的更新。优化性能: 在性能敏感的场景下,使用
useCallback
可以优化组件的性能,避免不必要的计算。
用法举例:
import React, { useCallback, useState } from 'react';
const ExampleComponent: React.FC = () => {
const [text, setText] = useState<string>('');
const [count, setCount] = useState<number>(0);
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setText(event.target.value);
}, []);
const handleIncrement = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<input type="text" value={text} onChange={handleChange} />
<p>Text: {text}</p>
<p>Count: {count}</p>
<button onClick={handleIncrement}>Increment Count</button>
</div>
);
};
export default ExampleComponent;
在这个示例中,我们使用 useCallback
缓存了处理输入框变化和增加计数的回调函数 handleChange
和 handleIncrement
。这样,在每次父组件重新渲染时,这两个回调函数不会被重新创建,从而避免不必要的性能消耗。
通过使用 useCallback
,我们可以确保只在依赖项发生变化时重新创建回调函数,提高性能并避免不必要的更新。这对于传递给子组件的回调函数或在性能敏感的场景下非常有用。感谢您的指正,希望这个例子更符合您的要求。
useTransition
useTransition
是 React 18 中引入的一个 Hook,用于管理 UI 中的过渡状态,特别是在处理长时间运行的状态更新时。它允许你将某些更新标记为“过渡”状态,这样 React 可以优先处理更重要的更新,比如用户输入,同时延迟处理过渡更新。
参数说明:
useTransition
不需要任何参数
返回说明:
useTransition
返回一个数组,包含两个元素isPending
(boolean),告诉你是否存在待处理的 transition。startTransition
(function) 函数,你可以使用此方法将状态更新标记为 transition。
应用场景:
优化用户体验: 在数据加载或其他过渡时显示加载指示器,提高用户体验。
控制交互: 在需要进行一些耗时操作时,可以使用
useTransition
控制界面的交互,避免用户误操作。
用法举例:
想象一个场景,页面上有多个tab,其中一个请求耗时较长,我们快速点击tab切换内容,但总是会在请求耗时的tab上卡顿一下,代码如下:
import React, { useState, memo } from 'react';
const TabContainer = () => {
const [tab, setTab] = useState('about');
// 核心:切换选项卡
function selectTab(nextTab) {
setTab(nextTab);
}
return (
<div>
<div>
<TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>About</TabButton>
<TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>Posts (slow)</TabButton>
<TabButton isActive={tab === 'contact'} onClick={() => selectTab('contact')}>Contact</TabButton>
</div>
<hr />
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
{tab === 'contact' && <ContactTab />}
</div>
);
};
const PostsTab = memo(() => {
let items = [];
for (let i = 0; i < 500; i++) {
items.push(<SlowPost key={i} index={i} />);
}
return <ul>{items}</ul>;
});
const SlowPost = ({ index }) => {
let startTime = performance.now();
while (performance.now() - startTime < 10) { }
return <li>Post on weijunext.com #{index + 1}</li>;
};
// 省略非关键代码
// ……
export default TabContainer;
如果我们想用useTransition
保持UI的响应,只需要用startTransition
包裹切换选项卡的set
const [isPending, startTransition] = React.useTransition();
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
这样我们快速切换tab,无论点到哪一个tab都不会卡顿。
注意事项:
在React18中,不支持 async 和 await,但是从React19开始,已经支持 async 和 await 了
在 React18 中的用法
// 错误做法
startTransition(() => {
// ❌ 在调用 startTransition 后更新状态
setTimeout(() => {
setPage('/about');
}, 1000);
});
// 正确做法
setTimeout(() => {
startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
setPage('/about');
});
}, 1000);
// 错误做法
startTransition(async () => {
await someAsyncFunction();
// ❌ 在调用 startTransition 后更新状态
setPage('/about');
});
// 正确做法
await someAsyncFunction();
startTransition(() => {
// ✅ 在调用 startTransition 中更新状态
setPage('/about');
});
在 React19 中的用法
import {useState, useTransition} from 'react';
import {updateQuantity} from './api';
function CheckoutForm() {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);
// 支持 await async
function onSubmit(newQuantity) {
startTransition(async function () {
const savedQuantity = await updateQuantity(newQuantity);
startTransition(() => {
setQuantity(savedQuantity);
});
});
}
// ……
}
原理剖析:
useTransition 的核心原理是将一部分状态更新处理为低优先级任务,这样可以将关键的高优先级任务先执行,而低优先级的过渡更新则会稍微延迟处理。这在渲染大量数据、进行复杂运算或处理长时间任务时特别有效。React 通过调度机制来管理优先级:
高优先级更新:直接影响用户体验的任务,比如表单输入、按钮点击等。
低优先级更新:相对不影响交互的过渡性任务,比如大量数据渲染、动画等,这些任务可以延迟执行。
+-----------------------+
| App |
| |
| +--------------+ |
| | Input | |
| +--------------+ |
| |
| +--------------+ |
| | Display | |
| +--------------+ |
+-----------------------+
用户输入
|
v
[高优先级更新] ---> [调度器] ---> [React 更新组件]
|
+---> [低优先级过渡更新] --> [调度器] --> [等待处理]
useDeferredValue
useDeferredValue
是 React 18 中引入的一个 Hook,用于延迟获取某个值,以提高性能。它可以将某个值的获取推迟到下一个渲染周期,从而避免在当前渲染周期内进行不必要的计算。
参数说明:
value
: 要延迟获取的值。
返回说明:
返回一个代表延迟获取的值的对象。
应用场景:
性能优化: 在某些情况下,获取某个值可能会引起性能问题,通过延迟获取可以避免不必要的计算,提高性能。
优化渲染: 在某些值不是立即需要的情况下,可以延迟获取以避免不必要的渲染。
用法举例:
import React, { useDeferredValue, useState } from 'react';
const ExampleComponent: React.FC = () => {
const [count, setCount] = useState<number>(0);
const deferredCount = useDeferredValue(count);
const incrementCount = () => {
setCount((prevCount) => prevCount + 1);
};
return (
<div>
<p>Deferred Count: {deferredCount}</p>
<p>Actual Count: {count}</p>
<button onClick={incrementCount}>Increment Count</button>
</div>
);
};
export default ExampleComponent;
在这个示例中,我们使用 useDeferredValue
将 count
的值延迟获取。在每次用户点击增加按钮时,count
的值会增加,但 deferredCount
的值会在下一个渲染周期才会更新。这样可以避免在当前渲染周期内进行不必要的计算,提高性能。
通过使用 useDeferredValue
,我们可以延迟获取某个值,从而优化性能并减少不必要的渲染。这对于一些不是立即需要的值的场景非常有用。希望这个例子能够帮助您理解 useDeferredValue
的用法和应用场景。
其他 Hook
useSyncExternalStore
useSyncExternalStore
是 React 18 中引入的一个 Hook,用于从外部存储(如 Redux、MobX 或其他状态管理库)中同步获取数据。它提供了一种安全的方式来订阅外部存储的变化,并确保组件在外部存储更新时能够正确地重新渲染。
参数说明:
useSyncExternalStore
接受三个参数:
subscribe
: 一个函数,用于订阅外部存储的变化。它应该返回一个取消订阅的函数。getSnapshot
: 一个函数,用于获取当前的快照值(即外部存储的当前状态)。getServerSnapshot
(可选): 在服务器端渲染时使用的函数,返回服务器快照的值。
返回说明:
返回外部存储的当前快照值。
应用场景:
与外部状态管理库集成: 在使用 Redux、MobX 等状态管理库时,可以使用
useSyncExternalStore
来同步外部状态。确保一致性: 确保在外部存储更新时,组件能够正确地重新渲染,避免不一致的状态。
用法举例:
一、订阅浏览器Api 实现自定义hook(useStorage)
我们实现一个useStorage Hook,用于订阅 localStorage 数据。这样做的好处是,我们可以确保组件在 localStorage 数据发生变化时,自动更新同步。
实现代码
我们将创建一个 useStorage Hook,能够存储数据到 localStorage,并在不同浏览器标签页之间同步这些状态。此 Hook 接收一个键值参数用于存储数据的键名,还可以接收一个默认值用于在无数据时的初始化。
在 hooks/useStorage.ts 中定义 useStorage Hook:
import { useSyncExternalStore } from "react"
/**
*
* @param key 存储到localStorage 的key
* @param defaultValue 默认值
*/
export const useStorage = (key: any, defaultValue?: any) => {
const subscribe = (callback: () => void) => {
window.addEventListener('storage', (e) => {
console.log('触发了', e)
callback()
})
return () => window.removeEventListener('storage', callback)
}
//从localStorage中获取数据 如果读不到返回默认值
const getSnapshot = () => {
return (localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)!) : null) || defaultValue
}
//修改数据
const setStorage = (value: any) => {
localStorage.setItem(key, JSON.stringify(value))
window.dispatchEvent(new StorageEvent('storage')) //手动触发storage事件
}
//返回数据
const res = useSyncExternalStore(subscribe, getSnapshot)
return [res, setStorage]
}
在 App.tsx 中,我们可以直接使用 useStorage,来实现一个简单的计数器。值会存储在 localStorage 中,并且在刷新或其他标签页修改数据时自动更新。
import { useStorage } from "./hooks/useStorage"
const App = () => {
const [val, setVal] = useStorage('data', 1)
return (<>
<h3>{val}</h3>
<button onClick={() => setVal(val + 1)}>设置val</button>
</>)
}
export default App
效果演示
值的持久化:点击按钮增加 val,页面刷新后依然会保留最新值。
跨标签页同步:在多个标签页打开该应用时,任意一个标签页修改 val,其他标签页会实时更新,保持同步状态。
二、订阅history实现路由跳转
实现一个简易的useHistory Hook,获取浏览器url信息 + 参数
import { useSyncExternalStore } from "react"
export const useHistory = () => {
const subscribe = (callback: () => void) => {
window.addEventListener('popstate', callback)
window.addEventListener('hashchange', callback)
return () => {
window.removeEventListener('popstate', callback)
window.removeEventListener('hashchange', callback)
}
}
const getSnapshot = () => {
return window.location.href
}
const push = (path: string) => {
window.history.pushState(null, '', path)
window.dispatchEvent(new PopStateEvent('popstate'))
}
const replace = (path: string) => {
window.history.replaceState(null, '', path)
window.dispatchEvent(new PopStateEvent('popstate'))
}
const res = useSyncExternalStore(subscribe, getSnapshot)
return [res, push, replace] as const
}
使用 useHistory Hook
让我们在组件中使用这个 useHistory Hook,实现基本的前进、后退操作以及程序化导航。
import { useHistory } from "./hooks/useHistory"
const App = () => {
const [history, push, replace] = useHistory()
return (<>
<div>当前url:{history}</div>
<button onClick={() => { push('/aaa') }}>跳转</button>
<button onClick={() => { replace('/bbb') }}>替换</button>
</>)
}
export default App
效果演示
history:这是 useHistory 返回的当前路径值。每次 URL 变化时,useSyncExternalStore 会自动触发更新,使 history 始终保持最新路径。
push 和 replace:点击“跳转”按钮调用 push("/aaa"),会将 /aaa 推入历史记录;点击“替换”按钮调用 replace("/bbb"),则会将当前路径替换为 /bbb。
注意事项
如果 getSnapshot
返回值不同于上一次,React 会重新渲染组件。这就是为什么,如果总是返回一个不同的值,会进入到一个无限循环,并产生这个报错。
Uncaught (in promise) Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
function getSnapshot() {
return myStore.todos; //object
}
这种写法每次返回了对象的引用,即使这个对象没有改变,React 也会重新渲染组件。
如果你的 store 数据是可变的,getSnapshot
函数应当返回一个它的不可变快照。这意味着 确实 需要创建新对象,但不是每次调用都如此。而是应当保存最后一次计算得到的快照,并且在 store 中的数据不变的情况下,返回与上一次相同的快照。如何决定可变数据发生了改变则取决于你的可变 store。
function getSnapshot() {
if (myStore.todos !== lastTodos) {
// 只有在 todos 真的发生变化时,才更新快照
lastSnapshot = { todos: myStore.todos.slice() };
lastTodos = myStore.todos;
}
return lastSnapshot;
}
useDebugValue
useDebugValue
是 React 18 中引入的一个 Hook,用于为自定义 Hook 提供调试信息。它可以帮助开发者在开发过程中更好地理解自定义 Hook 的工作原理和状态。
参数说明:
value
: 要为其提供调试信息的值。format
: 一个函数,用于格式化value
提供的调试信息。
返回说明:
无返回值。
应用场景:
自定义 Hook 调试: 在自定义 Hook 中使用
useDebugValue
提供更多有关 Hook 内部状态的调试信息。开发调试工具: 可以结合开发者工具使用,帮助开发者更好地理解 Hook 的工作原理。
用法举例:
import React, { useDebugValue, useState } from 'react';
const useCounter = (initialCount: number) => {
const [count, setCount] = useState(initialCount);
useDebugValue(count, (count) => `Current count: ${count}`);
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
return { count, increment };
};
const ExampleComponent: React.FC = () => {
const { count, increment } = useCounter(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment Count</button>
</div>
);
};
export default ExampleComponent;
在这个示例中,我们定义了一个自定义 Hook useCounter
,其中使用 useDebugValue
提供了关于 count
值的调试信息。在调试工具中,开发者可以看到当前的计数值,帮助他们更好地理解 Hook 的工作原理。
通过使用 useDebugValue
,我们可以为自定义 Hook 提供更多的调试信息,帮助开发者更好地理解 Hook 的状态和工作原理。这对于开发过程中的调试和问题排查非常有帮助。希望这个例子能够帮助您理解 useDebugValue
的用法和应用场景。
useId
useId
是 React 18 中引入的一个 Hook,主要用于生成唯一的 ID,通常用于支持无障碍(accessibility)特性和确保组件在多个实例中正确关联。它可以帮助你在渲染过程中生成稳定的 ID,避免了手动生成 ID 的复杂性和潜在的冲突。
参数说明:
useId
的基本用法非常简单。它不需要任何参数,返回一个字符串形式的唯一 ID。这个 ID 在组件的生命周期内是稳定的,即使组件重新渲染也不会改变。
返回说明:
useId
返回一个字符串,代表唯一的 ID。
使用示例:
以下是一个使用 useId
的简单示例,展示如何在表单中使用该 ID 来关联标签和输入字段:
import React, { useId } from 'react';
function CustomInput() {
const id = useId(); // 使用 useId 生成唯一 ID
return (
<div>
<label htmlFor={id}>Enter your name:</label>
<input id={id} type="text" />
</div>
);
}
function App() {
return (
<div>
<h1>Form with Unique IDs</h1>
<CustomInput />
<CustomInput /> {/* 这里会生成不同的 ID */}
</div>
);
}
export default App;
注意事项:
SSR 兼容性:
useId
在服务器端渲染(SSR)和客户端渲染(CSR)中都能保持一致性。它会确保在服务器和客户端生成相同的 ID,从而避免 ID 不一致的问题。多实例:在多个实例的情况下,
useId
会为每个实例生成不同的 ID,这对于组件复用非常有用。不适用于动态生成的 ID:如果你需要根据某些动态数据生成 ID,建议使用其他方法,因为
useId
的目的是生成稳定的、可重用的 ID。
useId
是一个非常实用的工具,特别是当你需要在组件中处理无障碍特性时,可以确保组件的可访问性和用户体验。
useOptimistic
useOptimistic
是一个 React Hook,它允许你在进行异步操作时显示不同 state。它接受 state 作为参数,并返回该 state 的副本,在异步操作(如网络请求)期间可以不同。你需要提供一个函数,该函数接受当前 state 和操作的输入,并返回在操作挂起期间要使用的乐观状态。
这个状态被称为“乐观”状态是因为通常用于立即向用户呈现执行操作的结果,即使实际上操作需要一些时间来完成。
参数说明:
state
:初始时和没有挂起操作时要返回的值。updateFn(currentState, optimisticValue)
:一个函数,接受当前 state 和传递给addOptimistic
的乐观值,并返回结果乐观状态。它必须是一个纯函数。updateFn
接受两个参数:currentState
和optimisticValue
。返回值将是currentState
和optimisticValue
的合并值。
返回说明:
optimisticState
:结果乐观状态。除非有操作挂起,否则它等于state
,在这种情况下,它等于updateFn
返回的值。addOptimistic
:触发乐观更新时调用的 dispatch 函数。它接受一个可以是任何类型的参数optimisticValue
,并以state
和optimisticValue
作为参数来调用updateFn
。
应用场景:
useOptimistic
Hook 提供了一种在后台操作(如网络请求)完成之前乐观地更新用户界面的方式。在表单的上下文中,这种技术有助于使应用程序在感觉上响应地更加快速。当用户提交表单时,界面立即更新为预期的结果,而不是等待服务器的响应来反映更改。例如,当用户在表单中输入消息并点击“发送”按钮时,
useOptimistic
Hook 允许消息立即出现在列表中,并带有“发送中……”标签,即使消息实际上还没有发送到服务器。这种“乐观”方法给人一种快速和响应灵敏的印象。然后,表单在后台尝试真正发送消息。一旦服务器确认消息已收到,“发送中……”标签就会被移除。
用法举例:
app.js
import { useOptimistic, useState, useRef } from "react";
import { deliverMessage } from "./actions.js";
function Thread({ messages, sendMessage }) {
const formRef = useRef();
async function formAction(formData) {
addOptimisticMessage(formData.get("message"));
formRef.current.reset();
await sendMessage(formData);
}
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [
...state,
{
text: newMessage,
sending: true
}
]
);
return (
<>
{optimisticMessages.map((message, index) => (
<div key={index}>
{message.text}
{!!message.sending && <small>(发送中……)</small>}
</div>
))}
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="你好!" />
<button type="submit">发送</button>
</form>
</>
);
}
export default function App() {
const [messages, setMessages] = useState([
{ text: "你好,在这儿!", sending: false, key: 1 }
]);
async function sendMessage(formData) {
const sentMessage = await deliverMessage(formData.get("message"));
setMessages((messages) => [...messages, { text: sentMessage }]);
}
return <Thread messages={messages} sendMessage={sendMessage} />;
}
actions.js
export async function deliverMessage(message) {
await new Promise((res) => setTimeout(res, 1000));
return message;
}
useActionState
useActionState
是一个可以根据某个表单动作的结果更新 state 的 Hook。
useFormStatus
useFormStatus
是一个提供上次表单提交状态信息的 Hook。