组件类的几个缺点。
- 大型组件很难拆分、重构、测试。
- 业务逻辑分散在组件的各个方法之中,导致重复逻辑、关联逻辑。
- 组件类引入了复杂的编程模式,比如 render props 和高阶组件。
hook的好处
- 在函数组件里“钩入” state 及生命周期等特性的函数
- 不需要很深的组件树嵌套
- 避免了 class 创建类实例和在构造函数中绑定事件处理器的成本。
- 在你不编写 class 的情况下使用 state 以及其他的 React 特性
- 使你在无需修改组件结构的情况下复用状态逻辑
- 将组件中相互关联的部分拆分成更小的函数
使用规则
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook
hook解决性能问题的方法
- 传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。
-
useCallback
Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate
继续工作:
// 除非 `a` 或 `b` 改变,否则函数引用地址不会变
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
-
useMemo
Hook 使得控制具体子节点何时更新变得更容易,减少了对纯组件的需要
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
- 最后,
useReducer
Hook 减少了对深层传递回调的依赖。
useState():状态钩子
- 声明一个可以在重渲染时保持状态的变量
- useState 唯一的参数就是初始 state,它会返回一对值:当前状态和一个让你更新它的函数。
- 那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。如果我们想要有条件地执行一个 effect,可以将判断放到 effect 的内部
import React, { useState } from "react";
export default function Button() {
const [buttonText, setButtonText] = useState("Click me, please");
function handleClick() {
return setButtonText("Thanks, been clicked!");
}
return <button onClick={handleClick}>{buttonText}</button>;
}
-
setState
的函数式更新形式结合展开运算符来达到合并更新对象的效果
const [state, setState] = useState({});
setState(prevState => {
// 也可以使用 Object.assign
return {...prevState, ...updatedValues};
});
-
setState
的函数式更新形式也允许我们指定 state 该 如何 改变而不用引用 当前 state
function Counter() {
// 下面的setCount不引用此处的count
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ 在这不依赖于外部的 `count` 变量
}, 1000);
return () => clearInterval(id);
}, []); // ✅ 我们的 effect 不使用组件作用域中的任何变量
return <h1>{count}</h1>;
}
- useState的函数形式可以惰性创建昂贵的初始state
function Table(props) {
// ⚠️ createRows() 每次渲染都会被调用
const [rows, setRows] = useState(createRows(props.count));
// ...
}
function Table(props) {
// ✅ createRows() 只会被调用一次
const [rows, setRows] = useState(() => createRows(props.count));
// ...
}
useEffect():副作用钩子
- 渲染结束之后开始执行
- 用来连接外部系统(不受react控制的),比如定时器、连接服务器、事件订阅等
- 使其状态与 React 组件的当前状态相匹配,比如用户设置放大缩小,通过effect同步到Map组件,Map.setZoom(level)
- 将不相关逻辑分离到不同的 effect 中,这样删除一个不会影响另一个
- React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。
- 它会在调用一个新的 effect 之前对前一个 effect 进行清理(比如切换连接聊天室、切换连接摄像头,数据请求(消除竞争条件的影响))
- 只有依赖项变化effect才会重新运行,如果传空数组那就只会调用一次
- 它的依赖包括 props 和直接在组件内声明的所有变量和函数。把函数或者对象移动到你的 effect 内部,减少对其依赖(每次渲染的对象都是不同的,容易导致effect频繁运行)。
- 我的 Effect 做了一些视觉相关的事情,在它运行之前我看到了一个闪烁,如果 Effect 一定要阻止浏览器绘制屏幕,使用
useLayoutEffect
替换 useEffect
。请注意,绝大多数的 Effect 都不需要这样。只有当在浏览器绘制之前运行 Effect 非常重要的时候才需要如此:例如,在用户看到 tooltip 之前测量并定位它。
- useLayoutEffect 可能会影响性能。尽可能使用useEffect
const Person = ({ personId }) => {
const [loading, setLoading] = useState(true);
const [person, setPerson] = useState({});
useEffect(() => {
setLoading(true);
fetch(`https://swapi.co/api/people/${personId}/`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(false);
});
}, [personId])
if (loading === true) {
return <p>Loading ...</p>
}
return (
<div>
<p>You're viewing: {person.name}</p>
<p>Height: {person.height}</p>
<p>Mass: {person.mass}</p>
</div>
)
}
useLayoutEffect
- 在react更新dom之后,浏览器绘制屏幕之前触发。
- 在浏览器重新绘制屏幕之前进行布局测量,比如toolTip的展示位置
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0); // 你还不知道真正的高度
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // 现在重新渲染,你知道了真实的高度
}, []);
// ... 在下方的渲染逻辑中使用 tooltipHeight ...
}
useCallback
- 缓存一个函数
- 在组件顶层调用 useCallback 以便在多次渲染中缓存函数,该回调函数仅在某个依赖项改变时才会更新
- 默认情况下,当一个组件重新渲染时, React 将递归渲染它的所有子组件
- useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
- useCallback 只应作用于性能优化。
- 请注意,useCallback 不会阻止创建函数。你总是在创建一个函数,但是如果没有任何东西改变,React 会忽略它并返回缓存的函数。
- memo包裹的组件在props没有变化时会跳过重新渲染
- 使用场景-优化子组件渲染速度
- 父组件传递给子组件一个函数,子组件使用了React.memo()包裹,如果父组件的其他state变化引发重新渲染,导致每次传递给子组件的函数都不一样(每次渲染生成新的函数)导致子组件也重新渲染,此时给父组件中的该函数使用useCallback包裹,使得函数地址没有变化(缓存起来的地址一样),子组件将跳过重新渲染。
// 没有缓存时,每次渲染重新生成函数
const submit = (a,b) => {
// doSomething....
}
// 使用useCallback缓存后,如果依赖没有变化,本次渲染的函数与上次渲染的函数地址一样(是同一个函数)
const submit = useCallback(
(a,b) => {
// doSomething....
},
[a, b],
);
// useCallback与useMemo的关系,在 React 内部的简化实现
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
- 场景-effect的依赖中存在函数时,将该函数使用useCallback包裹
- 场景-自定义hook
- 如果你正在编写一个 自定义 Hook,建议将它返回的任何函数包裹在
useCallback
中,这确保了 Hook 的使用者在需要时能够优化自己的代码。
useMemo
- 它在每次重新渲染的时候能够缓存计算的结果
- 把高开销的函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新执行函数。这种优化有助于避免在每次渲染时都进行高开销的计算。
// 将函数computeExpensiveValue执行结果缓存,而useCallback缓存的是函数
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- useMemo 也允许你跳过一次子节点的昂贵的重新渲染
- 手动将 JSX 节点包裹到
useMemo
中并不方便,比如你不能在条件语句中这样做。这就是为什么通常会选择使用 memo
包装组件而不是使用 useMemo
包装 JSX 节点
const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos])
- 不允许在循环中使用useMemo,可以抽出一个新组件,在新组件的顶层使用useMemo,或者你把新组件使用memo包裹,2种办法都可以使新组件跳过重新渲染
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
useDeferredValue
- 延迟更新 UI 的某些部分
- useDeferredValue 更适合优化渲染
- useDeferredValue 执行的延迟重新渲染默认是可中断的(输入改变后会中断这次渲染开始最新值的渲染),比如输入关键字后查询展示一个长list组件,输入时会卡顿(长列表渲染导致),此时可以延迟渲染list,可以立即更新输入框,list组件需要使用memo包裹,否则将会重新渲染
- 与防抖或节流不同,该方法可以自适应渲染速度,不必规定间隔时间(高性能电脑自动渲染快),并且可以避免让输入由于渲染列表而变得卡顿
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}
useRef
- useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。
- 本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。
- 请记住,当 ref 对象内容发生变化时,
useRef
并不会通知你。变更 .current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
- 改变 ref 不会触发重新渲染,所以 ref 不适合用于存储期望显示在屏幕上的信息
- 在 渲染期间 读取或写入 ref 会破坏这些预期行为,你可以在 事件处理程序或者 effects 中读取和写入 ref
function MyComponent() {
// ...
// 🚩 不要在渲染期间写入 ref
myRef.current = 123;
// ...
// 🚩 不要在渲染期间读取 ref
return <h1>{myOtherRef.current}</h1>;
}
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
// bad
function Image(props) {
// ⚠️ IntersectionObserver 在每次渲染都会被创建
const ref = useRef(new IntersectionObserver(onIntersect));
// ...
}
// good
function Image(props) {
const ref = useRef(null);
// ✅ IntersectionObserver 只会被惰性创建一次
function getObserver() {
if (ref.current === null) {
ref.current = new IntersectionObserver(onIntersect);
}
return ref.current;
}
// 当你需要时,调用 getObserver()
// ...
}
// 在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用
// 注意到我们传递了 [] 作为 useCallback 的依赖列表。
// 这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
useContext():共享状态钩子
- 深层传递值给子孙组件,最好提供一个默认值(即创建context时传入的值)
- 为了确定 context 值,React 搜索组件树,为这个特定的 context 向上查找最近的 context provider。
- 通过传递一个set函数给子孙组件以在合适的时机修改context(声明为一个state)
- 在传递对象和函数时优化重新渲染,比如传递的value为obj= {login:login,userId:userId}
- 使用useCallback缓存login函数
- 使用useMemo缓存对象obj
- 通过在 provider 中使用不同的值(下层使用不同的value即可覆盖上层的value)包装树的某个部分,可以覆盖该部分的 context。(类似css的color属性,下层会覆盖上层的属性)
const AppContext = React.createContext({});
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>
// 任何子组件都可读取AppContext
const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>{username}</p> // superawesome,值与Provider 的value有关
</div>
)
}
// 任何子组件都可读取AppContext
const Messages = () => {
const { username } = useContext(AppContext)
return (
<div className="messages">
<p>1 message for {username}</p> // superawesome,值与Provider 的value有关
</div>
)
}
useReducer():action 钩子
-
dispatch
函数 是为下一次渲染而更新 state。因此在调用 dispatch
函数后读取 state 并不会拿到更新后的值,也就是说只能获取到调用前的值。
- 通过给 useReducer 的第三个参数传入 初始化函数 来解决 重新创建初始值的 问题。
const [state, dispatch] = useReducer(reducer, initState, createInitialState);
- 如果你需要获取更新后的 state,可以手动调用 reducer 来得到结果
// reducer.js
const myReducer = (state, action) => {
switch(action.type) {
case('countUp'):
// 要返回一个新对象,而不是修改对象(state.count = state.count + 1)
return {
...state,
count: state.count + 1
}
default:
return state;
}
}
// app.js
function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 });
return (
<div className="App">
<button onClick={() => dispatch({ type: 'countUp' })}>
+1
</button>
<p>Count: {state.count}</p>
</div>
);
}
useImperativeHandle
- 它能让你自定义由 ref 暴露出来的句柄。
-
useImperativeHandle
应当与 forwardRef
一起使用
- 使用场景:例如,滚动到指定节点、聚焦某个节点、触发一次动画,以及选择文本等等
// 父组件
import ProTable from './ProTable';
const tableRef = useRef();
const refreshTable = () => {
// 父组件可以调用暴露出来的方法
tableRef.current?.refresh();
};
<ProTable ref={tableRef} onClick={() => refreshTable} />
//子组件暴露一个refresh方法
const ProTable = (props,ref) => {
useImperativeHandle(ref, () => ({
refresh,
}));
return xxx
}
export default forwardRef(ProTable);
useId
const id = useId()
<label htmlFor={id + '-firstName'}>名字:</label>
<input id={id + '-firstName'} type="text" />
自定义 Hook
- 在组件之间重用一些状态逻辑
- 自定义 Hook 必须以 “use” 开头
- 在两个组件中使用相同的 Hook 不会共享 state