一、产生原因
1、组件之间很难复用状态逻辑
2、复杂组件变得难以理解
3、难以理解的 class
4、更符合 FP 的理解, React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数
react-hooks思想更趋近于函数式编程,写起来更有函数即组件。
UI = render(data) 或者 UI = f(data)
react 组件逻辑复用的发展历程:
mixin -> HOC & render-props -> Hook
mixin 是 React 中最早启用的一种逻辑复用方式,因为它的缺点实在是多到数不清,而后面的两种也有着自己的问题,比如增加组件嵌套啊、props 来源不明确啊等等。可以说到目前为止,Hook 是相对完美的一种方案。
二、Hook API
useState
const [state, setState] = useState(initialState);
setState(newState);
复制代码
惰性初始化 state
initialState
参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => {
const initialState = someExpensiveComputation(props);
return initialState;
});
复制代码
? useState
中的更新函数与this.setState
的区别?
前者不会把新的 state 和旧的 state 进行合并。Hook 不能在 class 组件中使用
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState
。该函数将接收先前的 state,并返回一个更新后的值。
useReducer
useEffect
功能上类似 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
,将它们合并成了一个 API。默认情况下,React 会在第一次渲染之后和每次更新之后都会执行。
useEffect(effect,array);
复制代码
大多数使用的场景,是部分数据变更,然后去触发副作用(类似vue中的watch
,去监听部分数据的变化来做一定的操作)所以useEffect
提供了第二个参数array,来声明这个useEffect
依赖了哪些变量,当这些变量变化时才会触发这个副作用。
effect 每次完成渲染之后触发, 配合 array 去模拟类的生命周期
- 如果不传,则会在任何Dom变更之后执行;
- [],则只会在初始化的时候执行一次 ( componentDidMount )
- [id] 仅在 id 的值发生变化以后触发
- 清除effect (通过 return 返回一个函数)
useEffect(() => {
const subscription = props.source.subscribe();
return () => { // 清除订阅
subscription.unsubscribe();
};
});
复制代码
总结
有时我们会遇到尽管在effect中使用到了,但是其实并不是我们需要监听或者依赖的项,比如一个函数我们在effect中调用了,但实际上也只是调用了一下,并没有其他,对于这种虽然在effect中使用了但实际不是依赖项的,我们尽量使用其他方法来绕过。比如
- 对于一些变量不希望引起 effect 重新更新的,使用 ref 解决。
- 对于获取状态用于计算新的状态的,尝试
setState
的函数入参,或使用useReducer
整合多个类型的状态; - 对于函数,使用
useCallback
避免重复创建。 - 对于对象或者数组,则可以使用
useMemo
,从而减少 deps 的变化。
官方推荐我们把这些依赖了state或者props的函数放到useEffect中去声明,这样对于useEffect,可以明确知道整个useEffect依赖的state和props。比如下面这种异步获取数据:
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);//使用productId prop
const json = await response.json();
setProduct(json);
}
useEffect(() => {
fetchProduct();
}, []); // ? 这样是无效的,因为 `fetchProduct` 使用了 `productId` // ...
}
//修改为
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
useEffect(() => {
// 把这个函数移动到 effect 内部后,我们可以清楚地看到它用到的值。
async function fetchProduct() {
const response = await fetch('http://myapi/product/' + productId);
const json = await response.json();
setProduct(json);
}
fetchProduct();
}, [productId]); // ✅ 有效,因为我们的 effect 只用到了 productId
// ...
}
复制代码
useLayoutEffect
使用和useEffect
基本一致,只是执行的时机是在DOM更新后立即调用,而不是渲染完之后。
- 通过同步执行状态更新可解决一些特性场景下的页面闪烁问题
- useLayoutEffect 会阻塞渲染,请谨慎使用 对比看看
import React, { useLayoutEffect, useEffect, useState } from 'react';
import './App.css'
function App() {
const [value, setValue] = useState(0);
useEffect(() => {
if (value === 0) {
setValue(10 + Math.random() * 200);
}
}, [value]);
const test = () => {
setValue(0)
}
const color = !value ? 'red' : 'yellow'
return (
<React.Fragment>
<p style={{ background: color}}>value: {value}</p>
<button onClick={test}>点我</button>
</React.Fragment>
);
}
export default App;
复制代码
useMemo
通过一些变量计算得到新的值。通过把这些变量加入依赖 deps,当 deps 中的值均未发生变化时,跳过这次计算,某种意义上类似vue中的**computed
**。这种优化有助于避免在每次渲染时都进行高开销的计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码
- memo 是在 DOM 更新前触发的,就像官方所说的,类比生命周期就是 shouldComponentUpdate
useCallback
useCallback 用法和 useMemo 类似,用来缓存函数实例
useCallback(fn, deps)
相当于 useMemo(() => fn, deps)
。
const memoizedCallback = useCallback( () => {
doSomething(a, b);
},[a, b]);
复制代码
useContext
useContext
让你不使用组件嵌套就可以订阅 React 的 Context。
// 1、在上层组件树中创建 Context;
const MyContext = React.createContext(themes);
// 2、使用 <MyContext.Provider> 来为下层组件提供 context
<MyContext.Provider value={themes}>
...
</MyContext.Provider>
// 3、下层组件读取 context (这个地方相比之前有变化)
const value = useContext(MyContext);
复制代码
官方提示:
useContext(MyContext)
相当于 class 组件中的 static contextType = MyContext
或者 <MyContext.Consumer>
。
useContext(MyContext)
只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context。
useRef
const refContainer = useRef(initialValue);
复制代码
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。
本质上,useRef
就像是可以在其 .current
属性中保存一个可变值的“盒子”。
- 解决引用问题–useRef 会在每次渲染时返回同一个 ref 对象;
- 解决一些 this 指向问题;
- 对比 createRef — 在初始化阶段两个是没区别的,但是在更新阶段两者是有区别的;
- 我们知道,在一个局部函数中,函数每一次 update,都会在把函数的变量重新生成一次。 所以我们每更新一次组件, 就重新创建一次 ref, 这个时候继续使用 createRef 显然不合适,所以官方推出 useRef。useRef 创建的 ref 仿佛就像在函数外部定义的一个全局变量,不会随着组件的更新而重新创建。但组件销毁,它也会消失,不用手动进行销毁
总结下就是 ceateRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
复制代码
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与 forwardRef
一起使用
Hook 使用规则
Hook 就是 JavaScript 函数,但是使用它们会有两个额外的规则:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。
三、React-Router 的 Hooks
5.1版本的React-Router,带来了useHistory,useLocation,useParams,useRouteMatch四个钩子函数。
useHistory
function BackButton() {
let history = useHistory();
return (
<>
<button type="button" onClick={() => history.push("/blog/1")}>
123
</button>
<button type="button" onClick={() => history.goBack()}>
回去
</button>
</>
);
}
复制代码
useParams
之前 必须使用match
来获取路由中的params
;
useParams
返回URL参数的key/value的对象。 使用它来访问当前的match.params。useParams
的组件中也不用再写{match}
了。