目录
在学习Hook之前,先了解hook是什么,为什么要用hook
hook是什么?
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,例如部分的生命周期函数。需要注意的是Hook不能在类组件中使用。
也就是说,使用hook就是为了在函数式组件中使用到类组件中的某些特性。
Q:那为什么要用hook,为什么不直接创建类组件?
那么,带着这个问题往下看
useState
useState接收一个初始值,返回的是一个解构出来的数组,第一个是当前状态(似state),第二个是状态的更新函数(似setState),更新函数与类组件的setState不同的是,useState的更新函数会将状态替换(replace)而不是合并(merge)。
使用场景:函数组件中需要用到状态值,类似react类组件的state
在以下例子当中,没有用到类组件中易变的this,没有this指向问题,代码简单明了
import React, { useState } from 'react';
function Example() {
// 声明一个新的叫做 “count” 的 state 变量,0是默认值
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Example;
useState后面可以传值也可以是个函数
const [ count, setCount ] = useState(() => {
return props.count || 0
})
当我们在使用 useState 更新值时,组件重新渲染。若传入的更新值时值不变时,组件不会重新渲染。
useEffect
使用场景:useEffect副作用,使函数组件拥有了类似react的声明周期。useEffect会在组件每次render之后调用。使用useEffect 可以更好的处理异步请求。
熟悉React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合
useEffect(() => {}, [array]);
useEffect 可以接收两个参数,第一个是执行函数,可以return。第二个参数为数组,数组中有指定的参数值时,会根据该参数改变调用。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
const [dataSources, setDataSources] = useState([]);
/*
* 情况一:useEffect无第二个参数
*/
//组件初始化和render后,都会执行该useEffect
useEffect(() => {
console.log("相当于生命周期:componentDidMount+componentDidUpdate")
});
/*
* 情况二:useEffect有第二个参数
*/
//第二个参数为空数组时:组件初始化才执行
useEffect(() => {
console.log("相当于生命周期:componentDidMount");
}, []);
//第二个参数为指定状态值时:组件初始化时和dataSources发生变化才执行
useEffect(() => {
console.log("相当于生命周期:componentDidMount")
console.log("相当于依赖dataSources状态值的生命周期:componentDidUpdate")
}, [dataSources]);
//执行函数内return一个函数:初始化时执行函数体,组件卸载unmount时执行return后的函数
useEffect(() => {
console.log("相当于生命周期:componentDidMount")
// 执行函数中直接使用return返回一个函数,这个函数会在组件unmount时执行。
return () => {
console.log('相当于声明周期:componentWillUnmount');
}
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Example;
与 类组件中的componentDidMount 或 componentDidUpdate 不同,useEffect是异步的,使用useEffect 调度的 effect不会阻塞浏览器更新屏幕
useReducer
使用场景:由于useState的更新函数采用的是替换的方式,当我们要在函数组件中处理复杂状态时,我们可以使用useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);
跟redux中的reducer类似,有个配套使用的dispatch
import React, { useReducer } from 'react';
// 修改一个状态对象中的某属性,如果用useState,则要重新赋值一个对象,每个属性名都要写一遍
const initialState = {count: 0, name: '名字', age: 1};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {...state, count: state.count + 1};
case 'decrement':
return {...state, count: state.count - 1};
default:
throw new Error();
}
}
function Example() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
export default Example;
init是个函数,并以initialArg为入参,用于操作initialArg,返回自定义的初始值。
import React, { useReducer } from 'react';
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Example({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
export default Example;
useContext
使用场景:如果需要在深层次组件之间共享状态,可以使用 useContext()。context 提供了一种在组件之间共享 props 的方式,而不必显示地通过组件树的逐层传递 props; useContext 钩子比原始 class 组件中使用 context 更为方便;
import React, { useContext } from 'react';
const TestContext = React.createContext({});
const Navbar = () => {
const { username } = useContext(TestContext);
return (
<div className="navbar">
<p>{username}</p>
</div>
);
};
const Messages = () => {
const { messageDetail } = useContext(TestContext);
return (
<div className="messages">
<p>1 message for {messageDetail}</p>
</div>
);
};
const App = () => {
return (
<TestContext.Provider
value={{
username: 'superawesome',
messageDetail: 'hello world',
}}
>
<div className="test">
<Navbar />
<Messages />
</div>
</TestContext.Provider>
);
};
export default App;
当然在类组件中 ,provider提供的值也可以声明static contextType后直接用this.context获取,但是这种方法仅限于类组件,不支持多个Context嵌套。
相比之下useContext()的方式,最为简单。
useCallback
使用场景:useCallback用来缓存方法,类似react组件构造函数里面定义的:this.onChange =
this.onChange.bind(this),返回一个缓存的函数
先看React.memo的使用场景
//使用场景:React.memo就类似React.PureComponent,专门用于纯函数组件,起作用是对内部对象进行
//浅比较,判断是否重新渲染。
import React, { useState, useCallback } from 'react';
function Example() {
const [count, setCount] = useState(0);
const [list, setList] = useState([]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>addCount</button>
<button onClick={() => setList([...list, 1])}>changeList</button>
<Child list={list} />
</div>
);
}
此时,无论是改变count状态值,还是name状态值,Child组件都会重新渲染
//const Child = (props) => {
// console.log("进入了组件child")
// return (
// <div>这里是child:list为{props.list.join(',')}</div>
// )
//}
使用React.memo,就可以根据父组件传入的props也就是list是否更新来进行渲染
const Child = React.memo((props) => {
console.log("进入了组件child")
return (
<div>这里是child:{props.list.join(',')}</div>
)
})
export default Example;
接着上面的例子,我们在子组件上加入一个函数
const handleChange = () => {
console.log(`selected`);
}
<Child list={list} handleChange={handleChange} />
此时,child组件又出现了同样的问题,现在就要用useCallback()来解决这个问题,这时状态值变化才重新改变handleChange方法
const handleChange = useCallback(() => {
console.log(`selected`);
},[])// 或[list]
useMemo
使用场景:useMemo函数用于缓存需计算操作的状态值,类似Vue的计算属性computed。
第一个参数为计算函数,且必须return返回一个结果,返回一个缓存的值,第二个参数是一个数组[],useMemo执行依赖于次数组的状态值
import React, { useState, useCallback, useMemo } from 'react';
function Example() {
const [count, setCount] = useState(0);
const [list, setList] = useState([]);
const [a, setA] = useState(0)
const [b, setB] = useState(0)
const handleChange = useCallback(() => {
console.log(`selected`);
},[])
//使用useMemo缓存一个计算值,计算函数的执行依赖于状态值a和b,当a和b变化时才执行计算函数
const memoizedValue = useMemo(() => a + b, [a, b]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>addCount</button>
<button onClick={() => setList([...list, 1])}>changeList</button>
<button onClick={() => { setA(a+1)}}>点我A</button>
<button onClick={() => { setB(b+1)}}>点我B</button>
<Child list={list} memoizedValue={memoizedValue} handleChange={handleChange} />
</div>
);
}
const Child = React.memo((props) => {
console.log("进入了组件child")
return (
<div>这里是child:list为{props.list.join(',')}, 计算总和:{props.memoizedValue}</div>
)
})
export default Example;
总结
回答为什么要使用hook
- 使状态逻辑复用变得简单可行:将组件状态逻辑提取到可重用的函数中,使得这些逻辑可以单独测试并复用,且使你在无需修改组件结构的情况下复用状态逻辑。
- 简化生命周期:在类组件当中按照严格的生命周期函数来进行创建(componentDidMount)销毁(componentWillUnmount),或者说在更新(componentDidUpdate)时常常遇到几个不相关逻辑的复杂交错。解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到了几个不同方法中的问题
- 不用考虑 Class的this指向:在类组件中,为了解决 this 不符合预期的问题,还必须用 bind 或箭头函数。函数组件更加契合 React 框架的设计理念。
局限性(规则)
hook暂时还不能完全地为函数组件补齐类组件的能力,比如没有完整的生命周期钩子函数
在使用层面有着严格的规则约束:
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。