React学习整理(一) --hooks
React 内置hooks
Hook 可以帮助在组件中使用不同的 React 功能。你可以使用内置的 Hook 或使用自定义 Hook。本页列出了 React 中所有内置 Hook。
State Hook
useSate
用于给组件添加状态
1. 普通用法
可以设置初始值,通过结构的方式获取[变量,修改变量的函数]
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
console.log(count); // 0
setCount(count + 1);
console.log(count); // 0
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
注意: set函数调用后,直接获取,得到的还是调用前的值。只影响下一次useState返回的内容。
2. 根据上一个state更新state
只需要把set函数中内容设置为函数即可
function handleClick() {
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
setAge(age + 1); // setAge(42 + 1)
} // 结果为43
// 使用下面这种方式
function handleClick() {
setAge(a => a + 1); // setAge(42 => 43)
setAge(a => a + 1); // setAge(43 => 44)
setAge(a => a + 1); // setAge(44 => 45)
}// 结果为45
3. 更新对象和数组
在 React 中,状态被认为是只读的,因此
方法一:你应该替换它而不是改变现有对象
// 🚩 不要像下面这样改变一个对象:
form.firstName = 'Taylor';
// ✅ 使用新对象替换 state
setForm({
...form,
firstName: 'Taylor'
});
setPerson({
...person,
name: e.target.value
});
setTodos([
...todos,
{
id: nextId++,
title: title,
done: false
}
]);
方法二:可以使用Immer
先安装immer库
import { useImmer } from 'use-immer';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [list, updateList] = useImmer(initialList);
function handleToggle(artworkId, nextSeen) {
updateList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={list}
onToggle={handleToggle} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
...
</ul>
);
}
4. 更新函数
// ❌
const [fn, setFn] = useState(someFunction);
function handleClick() {
setFn(someOtherFunction);
}
// ✅
const [fn, setFn] = useState(() => someFunction);
function handleClick() {
setFn(() => someOtherFunction);
}
👎不建议写法
1. 在初始化状态,传入函数调用结果
尽管
createInitialTodos()
的结果仅用于初始渲染,但你仍然在每次渲染时调用此函数。如果它创建大数组或执行昂贵的计算,这可能会浪费资源。请注意,你传递的是
createInitialTodos
函数本身,而不是createInitialTodos()
调用该函数的结果。如果将函数传递给useState
,React 仅在初始化期间调用它。
function TodoList() {
// 👎不建议
const [todos, setTodos] = useState(createInitialTodos());
// 👍建议,传入函数名
const [todos, setTodos] = useState(createInitialTodos);
}
useReducer
useReducer同样是可以用来修改状态的,它更像是把一个state的各种更新的行为,放到一个函数来处理。
const [state, dispatch] = useReducer(reducer, initialArg, init?)
state
是变量,用于渲染。dispatch
是函数,通常需要传入一个对象(通常称为action),通常有一个type属性reducer
是一个函数,有两个参数,一个是state,另一个是dispatch中传入的action,并返回新的stateinitialArg
是作为初始化参数,如果init
函数存在,则state初始化参数为init(initialArg)
,否则是initialArg
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
const initialState = { name: 'Taylor', age: 42 };
export default function Form() {
const [state, dispatch] = useReducer(reducer, initialState);
function handleButtonClick() {
dispatch({ type: 'incremented_age' });
}
function handleInputChange(e) {
dispatch({
type: 'changed_name',
nextName: e.target.value
});
}
return (
<>
<input
value={state.name}
onChange={handleInputChange}
/>
<button onClick={handleButtonClick}>
Increment age
</button>
<p>Hello, {state.name}. You are {state.age}.</p>
</>
);
}
Context Hook
上下文帮助组件 从祖先组件接收信息,而无需将其作为 props 传递。
useContext
通过
createContext()
创建Xxxcontext
上下文,使用<Xxxcontext.Provider>
进行包裹,在后面的子孙组件中,通过useContext(Xxxcontext)
获取到内容
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
const CurrentUserContext = createContext(null);
export default function MyApp() {
const [theme, setTheme] = useState('light');
return (
<MyProviders theme={theme} setTheme={setTheme}>
<WelcomePanel />
<label>
...
</label>
</MyProviders>
);
}
function MyProviders({ children, theme, setTheme }) {
const [currentUser, setCurrentUser] = useState(null);
return (
<ThemeContext.Provider value={theme}>
<CurrentUserContext.Provider
value={{
currentUser,
setCurrentUser
}}
>
{children}
</CurrentUserContext.Provider>
</ThemeContext.Provider>
);
}
function WelcomePanel({ children }) {
const {currentUser} = useContext(CurrentUserContext);
return (
...
);
}
function Greeting() {
const {currentUser} = useContext(CurrentUserContext);
return (
<p>You logged in as {currentUser.name}.</p>
)
}
function LoginForm() {
const {setCurrentUser} = useContext(CurrentUserContext);
return (
<>
...
</>
);
}
function Panel({ title, children }) {
const theme = useContext(ThemeContext);
const className = 'panel-' + theme;
return (
...
)
}
function Button({ children, disabled, onClick }) {
const theme = useContext(ThemeContext);
const className = 'button-' + theme;
return (
...
);
}
Ref Hook
ref 允许组件 保存一些不用于渲染的信息,比如 DOM 节点或 timeout ID。与状态不同,更新 ref 不会重新渲染组件。ref 是从 React 范例中的“脱围机制”。当需要与非 React 系统如浏览器内置 API 一同工作时,ref 将会非常有用。
useRef
它能帮助引用一个不需要渲染的值。
【特点】
- 不会触发重复渲染
- 带有current属性,可以即时更新,常用来作为不需要渲染的一些变量
const ref = useRef(initialValue)
function handleStartClick() {
const intervalId = setInterval(() => {
// ...
}, 1000);
intervalRef.current = intervalId;
}
ref操作dom
import { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
// ...
// ...
return <input ref={inputRef} />;
当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为 ref 对象的 current
属性。现在可以借助 ref 对象访问 <input>
的 DOM 节点,并且可以调用类似于 focus()
的方法:
function handleClick() {
inputRef.current.focus();
}
useImperativeHandle
可以用来自定义由 ref 暴露出来的句柄
例如下方,给父组件暴露focus和scrollIntoView方法
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input {...props} ref={inputRef} />;
});
import { useRef } from 'react';
import MyInput from './MyInput.js';
export default function Form() {
const ref = useRef(null);
function handleClick() {
ref.current.focus();
// 下方代码不起作用,因为 DOM 节点并未被暴露出来:
// ref.current.style.opacity = 0.5;
}
return (
<form>
<MyInput placeholder="Enter your name" ref={ref} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
Effect Hook
Effect 允许组件 连接到外部系统并与之同步。这包括处理网络、浏览器、DOM、动画、使用不同 UI 库编写的小部件以及其他非 React 代码。
有些组件需要与网络、某些浏览器 API 或第三方库保持连接,当它们显示在页面上时。这些系统不受 React 控制,所以称为外部系统。
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
useEffect
Effect 是从 React 范式中的“脱围机制”。避免使用 Effect 协调应用程序的数据流。如果不需要与外部系统交互,那么 可能不需要 Effect。
- 如果你的 Effect 确实没有使用任何响应式值,则它仅在 初始渲染后 运行。
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []);
- 不设置依赖项,Effect 会在组件的 每次单独渲染(和重新渲染)之后 运行。
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
});
在 Effect 中根据先前 state 更新 state
// ❌
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // 你想要每秒递增该计数器...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... 但是指定 `count` 作为依赖项总是重置间隔定时器。
// ...
}
// 👍
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1); // ✅ 传递一个 state 更新器
}, 1000);
return () => clearInterval(intervalId);
}, []); // ✅现在 count 不是一个依赖项
return <h1>{count}</h1>;
}
useLayoutEffect
useLayoutEffect
是 useEffect
的一个版本,在浏览器重新绘制屏幕之前触发。
大多数组件不需要依靠它们在屏幕上的位置和大小来决定渲染什么。他们只返回一些 JSX,然后浏览器计算他们的 布局(位置和大小)并重新绘制屏幕。
有时候,这还不够。想象一下悬停时出现在某个元素旁边的 tooltip。如果有足够的空间,tooltip 应该出现在元素的上方,但是如果不合适,它应该出现在下面。为了让 tooltip 渲染在最终正确的位置,你需要知道它的高度(即它是否适合放在顶部)。
要做到这一点,你需要分两步渲染:
- 将 tooltip 渲染到任何地方(即使位置不对)。
- 测量它的高度并决定放置 tooltip 的位置。
- 把 tooltip 渲染放在正确的位置。
所有这些都需要在浏览器重新绘制屏幕之前完成。你不希望用户看到 tooltip 在移动。调用 useLayoutEffect
在浏览器重新绘制屏幕之前执行布局测量:
import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';
export default function Tooltip({ children, targetRect }) {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
console.log('Measured tooltip height: ' + height);
}, []);
let tooltipX = 0;
let tooltipY = 0;
if (targetRect !== null) {
tooltipX = targetRect.left;
tooltipY = targetRect.top - tooltipHeight;
if (tooltipY < 0) {
// 它不适合上方,因此把它放在下面。
tooltipY = targetRect.bottom;
}
}
return createPortal(
<TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
{children}
</TooltipContainer>,
document.body
);
}
useInsertionEffect
调用 useInsertionEffect
在任何可能需要读取布局的副作用启动之前插入样式:
let isInserted = new Set();
function useCSS(rule) {
useInsertionEffect(() => {
// 同前所述,我们不建议在运行时注入 <style> 标签。
// 如果你必须这样做,那么应当在 useInsertionEffect 中进行。
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
}
性能Hook
useMemo
useMemo
是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}
useCallback
useCallback
是一个允许你在多次渲染中缓存函数的 React Hook。
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
useTransition
useTransition
是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook。
简单理解就是,降低渲染优先级,让其他高优先级的内容先渲染。例如:图表组件在渲染时,在输入框中输入内容,那么输入框中的内容优先渲染,图表组件的可以后置渲染。
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('about');
function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
// ……
}
isPending
,告诉你是否存在待处理的 transition。startTransition
函数,你可以使用此方法将状态更新标记为 transition。
传递给
startTransition
的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。
在Transition中更新父组件
你也可以通过调用 useTransition
以更新父组件状态。例如,TabButton
组件在 Transition 中包装了 onClick
逻辑
export default function TabButton({ children, isActive, onClick }) {
const [isPending, startTransition] = useTransition();
if (isActive) {
return <b>{children}</b>
}
return (
<button onClick={() => {
startTransition(() => {
onClick();
});
}}>
{children}
</button>
);
}
useDefferredValue
useDeferredValue
是一个 React Hook,可以让你延迟更新 UI 的某些部分。
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// ...
}
如果是直接使用query值,则每次输入框有内容变化,则显示loading,而使用useDeferedValue,则会先保留更新前的值,之后再更新最新的值(可能会有点看起来像卡顿的感觉)
一个常见的备选 UI 模式是 延迟 更新结果列表,并继续显示之前的结果,直到新的结果准备好。调用 useDeferredValue
并将延迟版本的查询参数向下传递:
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
其他Hook
useDebugValue,useId,useSyncExternalStore,useActionState
转载自:https://juejin.cn/post/7376097985379926052