前言
在react 16.8之前,我们使用基于react的框架开发时,声明一个组件有两种方式:
- class声明组件
- 函数组件
在class 组件中,我们可以使用state、react的钩子函数等react特性。但是函数组件是一个无状态组件,而且对于操作DOM、发起请求等副作用并不能很好的处理。
什么是React Hooks
React Hooks 是react 16.8 的新特新,它可以让你在函数组件中使用state以及其他的react特性。hooks翻译为钩子,其含义就是向组件中钩入state,effect等。
react Hooks使用规则
在学习之前先跟大家说下react hooks使用的规则:
- 只能在react函数组件中使用Hooks。
- 只能在组件的最顶层使用Hooks
react Hooks具体使用
一、 useState
作用:在函数组件中调用它,可以添加一些内部的state。
示例:
import React,{useState} from 'react';
function Counter(){
const [count,setCount] = useState(0);
console.log(count)
return(
<div>
<button onClick={()=>{setCount(count+1)}}>+</button>
{count}
<button onClick={()=>{setCount(count-1)}}>-</button>
</div>
)
}
export default Counter;
使用useState:
const [state变量,修改该变量的set方法] = useState(初始值)
一般来说函数在执行结束后变量就会消失,但是react保留了state变量,当state发生改变后,函数组件都会重新渲染,useState都会将当前的state值传递给state变量。
setCount是一个useState返回的用于更新state变量的方法,接收state的新值作为参数。
下面是使用useState需要注意的地方:
-
每次setCount 都会重新渲染函数组件。
-
你可以设置多个useState,使用多个变量,但是useState也可以很好的接收 数组、对象作为初始值。
-
使用count声明state变量的原因是:使其只能被set函数进行修改,达到setState的效果。
-
setState()接收新的state或者一个返回state的函数(setCount(prevCount => prevCount - 1)})
-
在class组件中使用state,当setState时,是将setState的参数与原来的state进行对象的合并。但是useState则是使用新值替换原来的值。可能这么说不太明白,请看下面的示例:
import React,{useState} from 'react'; function Counter(){ const [people,setPeople] = useState({age:16,name:'buzhanhua'}); console.log(people.name) return( <div> <button onClick={()=>{setPeople({age:people.age+1})}}>+</button> {people.age} {people.name} <button onClick={()=>{setPeople({age:people+1})}}>-</button> </div> ) } export default Counter;
当你点击加好以后,你会发现people的name属性变为了undefined,证明该机制是 替换。
二、useEffect
作用:给函数组件增加操作副作用的能力。和class中的componentDidMount、componentDidUpdate、componentWillUnMount的用途一致。
那么是什么是副作用呢?副作用具体指的是什么呢?
那我们就应该提到两个概念了:
纯函数:指一个函数接受相同的参数,返回结果始终相同。也就是说函数不受外部变量的影响,同时也不影响外部变量,这样的函数我们称之为纯函数。
副作用:只要是跟函数外部环境发生交互,都称为副作用,指函数调用时,除了运行函数业务主逻辑,返回返回值之外,还对函数外部环境产生了影响,比如,数据,变量,屏幕输出等等,具体如操作DOM、http请求发起、设置订阅等。大致分为:不需要清除的副作用和需要清除的副作用。不需要清除就像操作DOM,如在组件渲染完成更改页面的title,改了就改了,并不需要清除什么。但是像设置的定时器、延时器等需要在组件卸载的时候进行清除,这种就是需要清除的副作用了。
示例 :
function UseEffect(){
const [age,setAge] = useState(0);
useEffect(()=>{
let time = setInterval(()=>{
setAge(age+1);
console.log(age)
},1000)
return ()=>{
clearInterval(time)
}
},[age])
return(
<div>
{age}
</div>
)
}
语法:
useEffect(function,arr);
注释:
-
useEffect接收两个参数,第一个为一个函数(必选),第二个是一个数组(可选);
-
参数一是一个函数,用于执行你想在在class组件不同生命周期钩子函数执行的一些代码。(下文细说)。
-
参数二是一个数组,由于state变量变化时,函数组件都会重新渲染,那么useEffect也会在每次渲染的时候都会执行,显然在某些条件下,你并不想让全部的useEffect都重新执行,这个参数就是加以限制,只有当列表的变量变化时,useEffect才会重新执行。
useEffect(()=>{ // 相当于class组件的componentDidMount和componentDidUpdate return ()=>{ //可选,该函数将在组件将要卸载时执行,相当于componentWillUnMount } },[age])
在class组件中,我们通常会在组件加载完成后,发起请求、操作DOM什么的,这种副作用是不需要清除的。useEffect的第一个参数函数,可以选择是否return一个函数,该函数将在组件将要卸载时执行。但是不同于class组件,在示例中,每次setAge都会重新渲染组件,也就会重复执行useEffect,它的执行是每次在重新渲染前将之前的定时器清除,重新渲染在开启新的定时器,这么做的目的是可以减少bug的出现。
useEffect Hook的第二个参数,是用来控制它什么时候执行的,有些情况我们只希望,监听挂载和卸载在组件的生命周期中只执行一次,比如: window上监听的事件,document.title 的改变等等。这些有可能不依赖任何的state , 那我们如何做到类似, componentDidMount 和 componentWillUnmount 的效果呢 ? 很简单, 如下
useEffect(()=>{
window.addEventListener('resize',onResize);
return ()=>{
window.removeEventListener('resize',onResize)
}
},[])
// 参数列表设置为空数组,这样只会在组件加载完成后绑定事件, 组件销毁前解绑事件
// 只会执行一次, 但是这种方式要谨慎使用, 亲测 : 这种方式 setInterval 会有问题
4. useEffect中定义的函数的执行不会阻碍浏览器更新视图,也就是说这些函数时异步执行的,而componentDidMonut和componentDidUpdate中的代码都是同步执行的。
三、useContext
在使用useContext之前,先说一下为什么要使用context。
我们在做react开发的过程中,组件之间共享值是肯定避免不了的一个问题,通常我们的做法是,通过props一级级传递,但是,组件共享的话如果不是父子级关系呢?你该如何传值呢?当然我们可以做到,但是会相当麻烦。
context,你可以理解为一个全局的state,里面可以存变量,方法,可以在provider之下的所有位置进行使用,不用关心层级关系。并可以通过里面的方法对值进行统一处理。
-
在react 16.8 之前我们使用context
import React ,{createContext,useState} from 'react'; const UserContext = new createContext(); // 创建一个context // 创建provider const UserProvider = props => { const [username,setUsername] = useState(''); return ( <UserContext.Provider value={{username,setUsername}}> {props.children} </UserContext.Provider> ) } // 创建Consumer const UserConsumer = UserContext.Consumer; // 使用Consumer进行包裹 const Pannel = ()=>( <UserConsumer> { ({username,setUsername})=>( <div> <div>user:{username}</div> <input onChange={e => setUsername(e.target.value)}/> </div> ) } </UserConsumer> ) const UseContext = ()=>( <UserProvider> <Pannel/> </UserProvider> ) export default UseContext;
我们可以看到,这种使用方式,在使用provider提供的变量和方法时,需要使用consumer进行包裹,并且需要使用一个函数接受、解构后才可以使用。当然在16.8之前不使用consumer包裹的方式,还可以使用contextType的方式进行使用,但是 16.8 以后这就简单了哦~
顺便说下contextType的使用方法吧:
import React,{createContext, Component,useState} from 'react';
const Context = new createContext();
const ContextProvider = (props)=>{
const [age,setAge] = useState(16)
console.log(age)
return(
<Context.Provider value={{age,setAge}}>
{props.children}
</Context.Provider>
)
}
class ContextType extends Component{
static contextType = Context;
render(){
return(
<div>
{this.context.age}
<button onClick={()=>{this.context.setAge(this.context.age+1)}}>长大吧宝贝</button>
</div>
)
}
componentDidMount(){
console.log(this.context) // {age: 16, setAge: ƒ}
}
}
const WarpContextType = ()=>(
<ContextProvider>
<ContextType/>
</ContextProvider>
)
export default WarpContextType;
contextType是一个静态属性,该属性会被赋予一个React.createContext的实例,那么你将可以在this.context中获取到距离当前组件最近层级的Context.Provider 中的值,你可以在任意的生命周期中使用它,包括render。很明显,它有一定缺陷,这种方式只能挂载一个Context。
-
16.8的函数组件的context
import React ,{createContext,useState,useContext} from 'react'; const UserContext = new createContext(); // 创造一个context // 创建provider const UserProvider = props => { const [username,setUsername] = useState(''); return( <UserContext.Provider value={{username,setUsername}}> {props.children} </UserContext.Provider> ) } const Pannel = () => { const {username,setUsername} = useContext(UserContext);// 使用context return ( <div> <div>user:{username}</div> <input onChange={e => setUsername(e.target.value)}/> </div> ) } const UseContext = ()=>( <UserProvider> <Pannel/> </UserProvider> ) export default UseContext;
在使用useContext后,就不需要再用consumer包裹取值了
四、useReducer
作用:其实就是useState的替代方案,现在useState将之前我们熟悉的state拆分开了,而这种方式则是将state组合起来,统一管理。
示例:
import React ,{useReducer} from 'react';
function reducer(state,action){
switch(action.type){
case 'add':
return{
...state,
count:state.count+1
}
case 'reduce':
return{
...state,
count:state.count-1
}
default:
return state
}
}
const initialState = {
count : 0,
name : 'buzhanhua'
}
function Count(){
const [state,dispatch] = useReducer(reducer,initialState);
return(
<div>
<button onClick={()=>{dispatch({type:'add'})}}>+</button>
{state.count}
<button onClick={()=>{dispatch({type:'reduce'})}}>-</button>
{state.name}
</div>
)
}
export default Count;
使用useReducer
const [state,dispatch] = useReducer(reducer,initialState);
有两种初始化state的方式,demo中是其中的一种,另一种是惰性初始化,但是不推荐使用,如果想了解详情,可参考官方文档。
五、useMemo和useCallback
这两个Hook的作用类似,主要作为性能优化手段进行使用,试想一下这个场景,当你使用set函数改变state时,函数组件将会重新进行渲染,但是你发现,并不是所有的子组件都有必要重新渲染,那么这两个Hook就是解决这种情况的,用于控制当依赖项数组中的state改变时才进行重新渲染。
示例:
import React,{useState,useMemo,useCallback} from 'react';
function Time(){
return(
<div>{Date.now()}</div>
)
}
function Count(){
const [count,setCount] = useState(0);
const [name,setName] = useState('buzhanhua');
//const memoizedComponent = useMemo(()=><Time/>,[count]);
const memoizedComponent = useCallback(<Time/>,[count]);
return(
<div>
<button onClick={()=>{setCount(count+1)}}>改变数字</button>
<button onClick={()=>{setName(name+'ha')}}>改变名字</button>
{memoizedComponent}
<p>数字:{count}</p>
<p>名字:{name}</p>
</div>
)
}
export default Count;
useCallback(fn,依赖项数组)等同于useMemo(()=>fn,依赖项数组)
下面对demo进行一些解释,在的demo中有两个button按钮,在使用useCallback或者useMemo后,你会发现只有当处于依赖项数组中的count改变时,Time组件才会重新渲染。
六、useRef
语法: const myRef = useRef(initialValue);
useRef返回一个可变的ref对象,其 .current属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。其实就是提供给我们一个保存可变变量的空间,大家想一下这个需求:怎么能够记录组件的渲染次数?其他的Hook都是一个常量,只有使用set函数的时候才可以改变,我们如果想实现该需求的话,需要找到一个可更改但又在改变时不会使组件从新渲染的东西。
下面我们先看一个我们熟悉的,获取DOM节点的demo:
import React,{useRef,useState} from 'react';
function UseRef(){
const myRef = useRef(null)
const callback = () =>{
myRef.current.focus()
}
return(
<div>
<input type="type" ref={myRef}/>
<button onClick={callback}>聚焦</button>
</div>
)
}
保存前一个状态
import React ,{useState,useEffect,useRef}from 'react';
function Rank(props){
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return(
<div>RankNow: {count}, before: {prevCount}
<button onClick={()=>{setCount(count + 1)}}>点击</button>
</div>
)
}
export default React.memo(Rank);
需求实现demo:
import React,{useRef,useState} from 'react';
function Time(props){
console.log(props.current)
return(
<div>{Date.now()}</div>
)
}
function UseRef(){
const myRef = useRef(0);
const [count,setCount] = useState(0);
myRef.current++
return(
<div>
<Time current={myRef.current}/>
<button onClick={()=>{setCount(count+1)}}>+</button>
</div>
)
}
export default UseRef
注释:
- 使用useRef方法返回的{current:...} 和自己创建一个{current:...} 对象的唯一区别就是,每次组件渲染返回的ref对象都是同一个对象,所以它可以实现上面的需求。
- useRef比ref属性更有用,它可以方便的保存任何可变值。
useImperativeHandle
自定义在使用ref时,公开给父组件的实例值, 必须和forwardRef一起使用
import React,{useImperativeHandle,forwardRef,useRef} from 'react';
function Input(props,ref){
const inputRef = useRef();
useImperativeHandle(ref,() => ({
focus: () => {
inputRef.current.focus()
}
}))
return(
<input type="text" ref={inputRef}/>
)
}
Input = forwardRef(Input);
function Rank(props){
const rankRef = useRef();
return(
<div>
<Input ref={rankRef}/>
<button onClick={() => {rankRef.current.focus()}}>点击</button>
</div>
)
}
export default React.memo(Rank);
七、自定义Hook
在react 16.8 之前,我们组件之间复用逻辑的方式,主要是通过props传递和高阶组件完成的,自定义Hook提供给我们另一种组件之间复用逻辑的新方式。
自定义 Hook 是一个函数,其名称以 “use” 开头(规定),函数内部可以调用其他的 Hook。
下面是一个获取浏览器窗口demo的展示,可以监测手动缩放
import React , {useState,useEffect} from 'react';
function WinSize(){
const [size,setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const onResize = ()=>{
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}
useEffect(()=>{
window.addEventListener('resize',onResize);
return ()=>{
window.removeEventListener('resize',onResize);
}
},[])
return size;
}
function Demo6(){
const size = WinSize();
return (
<div>
浏览器窗口:宽 {size.width} 高 : {size.height}
</div>
)
}
export default Demo6
踩坑
使用useEffect报错缺少依赖项
error : React Hook useLayoutEffect has a missing dependency: 'options'.
原因: 在启用eslint-plugin-react-hooks插件后, 一些配置项会强制提醒我们在使用effect的时候,申明所需要的依赖项。当useEffect里面使用外部的变量的情况,这些变量都属于依赖项,需要在第二个数组参数中放入对应的变量。
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
// ...
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn" // off
}
}
结束
本文为作者工作学习总结,如有错误或不当,请指出,欢迎共同学习