React-Hooks
1. React-hooks是什么
我们先来看一下官网的解释:
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
官网传送门
也就是说,react hook其实就是加强版的函数组件,能过通过hook获取class才有的东西,比如state和周期。
2. 为什么要用react-hooks
在没有hook的时候,如果我们要写一个有状态的组件:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
{this.state.count}
);
}
}
但是这种方式会存在问题:
① 不同组件间逻辑复用问题
当我们想要复用组件逻辑的时候
没有hook情况:
(1)使用render props:这是一个用于告知组件需要渲染什么内容的函数。
(2)或者高阶组件HOC,比如一些组件都需要订阅和取消订阅某些事件,使用HOC可以通过传递一个组件参数,还有一些其他参数,将组件包装在容器组件中来组成新组件,而且不修改原来的组件内容。
但是这些都会导致代码复杂,可能会形成嵌套地狱
当有了hook:
我们可以从组件中提取状态逻辑,使得可以复用,可以在无需修改组件结构的情况下复用。比如像订阅取消订阅行为,我们可以写一个自定义hook,名称都要以use开头,比如:
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
这个函数的作用是订阅好友状态,当我们在其他地方想要使用这个逻辑时候,就直接调用
② 复杂组件难以理解
不使用hook:
我们在组件的一个生命周期干很多事,比如发送请求,处理数据,监听订阅什么的一大堆,这些操作之间可能没有太大的关系,容易造成逻辑混乱,容易产生bug,给测试带来不便。
使用hook:
我们可以使用effect分离这些功能
③ class使用不方便
class中需要关心this的指向,还得记得绑定事件处理器,大量的class会导致热重载和性能问题。hook则可以在非class情况下使用更多的react特性。
2. 如何使用及使用规则
hook和现有的代码可以共存,不会影响原来的写法,只要你想用的时候,直接引入就可以了。它是一个特殊的函数,可以勾入react的特性。
①只在最顶层使用
不要在循环,条件或嵌套函数中调用 Hook
②只在react函数中调用
比如函数组件,自定义hook,不要在普通函数中调用
3. hook及应用场景
主要有几个比较常用的hook。
3.1 useState
当我们使用class对一个状态赋值,通常都是this.setState({"xxx": "xxx"}),当一个函数组件想要维护自己的state时候,发现只能转换成class,hook解决了这个问题
const [id, setId] = useState(0)
注意点:
初始值只在第一次有效,useState返回两个变量,第一个是变量名,第二个是设置这个变量名的方法。
注意点:
当初始值涉及到计算时候,我们可以给初始值设置为一个函数。
设置值的时候会触发render,除非设置的值和原来的一样就不会触发。
3.2 useEffect
useEffect接收一个函数还有一个数组
useEffect可以帮我们在函数里面使用class的生命周期,与class的周期对比:
比如:我们在組件挂载时候设置一个计时器,在组件卸载时候清除计时器,
- 对应class中componentDidMount和componentWillUnMount
使用effect则在函数中执行对应componentDidMount的代码,在retunr中执行对应componentWillUnMount的代码。
useEffect(() => {
...
return {...}
}, [])
- 对应class中的componentDidUpdate
useEffect(() => {
})
不加第二个参数表示每次更新时都会被执行
如果我们要监听某些变量变化了,再执行某些操作,则可以
useEffect(() => {
...
}, [name,age])
使用的注意点:
要确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量。
当effect执行时,我们会创建一个闭包,如果这个effect使用了外部的变量,且这个effect只被执行一次,我们又频繁改变这个变量,那么我们取得的值都不变。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // effect 依赖于 `count` state
}, 1000);
return () => clearInterval(id);
}, []);
return {count}
;
}
此时没有指定count为依赖,但是如果加上,计时器每次重新渲染都会被重新加载。就不符合我们的预期。
解决办法:
①使用函数式更新的方法
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // 不依赖于外部的count变量
}, 1000);
return () => clearInterval(id);
}, []);
return {count}
;
}
使用这种方式取到的count都是最新值。
② 使用useRef
useRef创建的对象,在生命周期中保持不变,那么就可以在上面定义方法
function Counter() {
let [count, setCount] = useState(0);
const myRef = useRef(null);
myRef.current = () => {
setCount(count + 1);
};
useEffect(() => {
let id = setInterval(() => {
myRef.current();
}, 1000);
return () => clearInterval(id);
}, []);
return {count}
;
}
③ 使用自定义hook
function useInterval(fun) {
const myRef = useRef(null);
useEffect(() => {
myRef.current = fun;
}, [fun]);
useEffect(() => {
const id = setInterval(() => {
myRef.current();
}, 1000);
return () => clearInterval(id);
}, []);
}
function Counter() {
let [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
});
return {count}
;
}
使用 React Hooks 声明 setInterval
④ 使用useReducer
function reducer(state, action) {
switch (action.type) {
case "increment":
return state + 1;
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, 0);
useEffect(() => {
const id = setInterval(() => {
dispatch({ type: "increment" });
}, 1000);
return () => clearInterval(id);
}, []);
return {state}
;
}
3.3 useContext
当跨层级的祖先组件要给孙子,曾孙组件传递数据时候,除了使用最粗暴的一层层prop往下套时,还可以使用createContext
: 在最外层套上provider,在用到的地方使用consumer。
const colorContext = React.createContext("gray");
function Bar() {
return {color => {color}} ;
}
function Foo() {
return ;
}
function App() {
return (
);
}
但是使用hook,我们就可以改写Bar:
function Bar() {
const color = useContext(colorContext);
return {color};
}
相比于createContext,useContext可以直接获取想要的值,十分简洁,而不需要嵌套一层,减少复杂的层级。
How the useContext Hook Works
3.4 useReducer
这个hook类似于redux,但是没有中间件,也可以用来替换useState。
在以下场景中,useReducer比useState更适用:
(1)state逻辑较复杂,且包含多个子值
(2)state之间有依赖
(3)优化触发深更新的组件,给子组件传递dispatch,而不是回调函数
官网的例子:
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
>
);
}
3.5 useCallback
useMemo解决了值的问题,那么callback就解决了函数的缓存问题,原理与useMemo类似。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
在类组件中我们会这么写:
class App {
render() {
return
{
console.log('dosomething');
}} />
;
}
}
缺点:一旦组件的props状态改变会触发重新渲染,产生新的style和doSomething,
在类组件中,我们还可以通过 this 这个对象来存储函数,而在函数组件中没办法进行挂载了。所以函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。
那么useCallback就产生了!
3.6 useMemo
场景:当我们在父组件定义了或计算了某些state,并将此传递给子组件,由于父组件的一系列操作,会导致不断被渲染,如果是使用const定义的变量,则会在渲染的时候重新生成新的内存空间,子组件接收这个变量,就会跟着重新渲染,虽然这个值一直都没有变。
这时候就可以使用useMemo,我们可以把他当成是对值的缓存,接收两个参数,第一个是一个函数,第二个是依赖数组。表示在某个依赖项改变时才会重新计算,有利于在每次渲染时候避免不必要的开销计算。
注意点:不要在函数内部执行与渲染无关的操作。如果需要,请放到useEffect中。
例如:
const data = useMemo(()=>{
return {
name
}
},[name])
memo为高阶组件,适用于函数组件,不适用class组件,使用方式:
React.memo(MyComponent, areEquald)
第一个参数是组件,第二个是比较方式,可选传,默认情况下,只会对prop进行浅比较,如果是一个对象,内存地址不变则不会重新渲染。官网说:此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。
如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染
https://github.com/happylindz/blog/issues/19
useCallback和useMemo的区别:
useMemo和useCallback接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于useMemo返回的是函数运行的结果,useCallback返回的是函数。
类似于生命周期中的shouldComponentUpdate,根据指定的依赖做出更新
3.7 useRef
useRef传入一个参数initValue,并创建一个对象{ current: initValue }给函数组件使用,在整个生命周期中该对象保持不变。
本质上,
useRef
就像是可以在其.current
属性中保存一个可变值的“盒子”。
使用useRef每次都会返回相同的引用,但是使用createRef创建的,每次渲染都会返回一个新的引用。
const [count, setCount] = useState(0)
const countRef = useRef(0)
useEffect(() => {
console.log('use effect...',count)
const timer = setInterval(() => {
console.log('timer...count:', countRef.current)
setCount(++countRef.current)
}, 1000)
return ()=> clearInterval(timer)
},[])
还可以用来做普通的DOM操作
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
>
);
}
精读《useRef 与 createRef 的区别》
4.不足
无法覆盖class component的所有生命周期
getSnapshotBeforeUpdate 和 componentDidCatch 目前覆盖不到
参考资料:
我们为什么要拥抱react hooks
十个案例学会React hooks
终于弄懂react hooks了!!!
why React hooks